Unity 音频卡顿 静帧 等待等问题的解决方案

2023-11-19

是否遇到过在Unity中加载音频文件卡顿(也就是画面卡住)的现象?特别是加载外部音频文件时。虽然时间很短,但这终归不是什么好现象,尤其是打游戏的话,影响很大。但是一些有牌面的Boss也不能不配音乐。

当然也可以通过其它方式解决,比如特定条件统一加载、切场景进度条之类的,但是程序员就要用程序的问题解决,毕竟这是一个被各个游戏和音乐播放器验证了无数遍的东西。

环境:
从本地或网络加载外部文件
Unity版本2020.3.30
Win10Unity编辑器

/// 规定几个特殊名词:
/// 等待:画面正常运行,但是正在读取音频数据,并没播放音乐,一般出现在切换加载网络音乐后(网络)             
/// 静帧:画面出现短暂静置,不刷新了,最直观的就是你放一个一直旋转的立方体,它会短暂的停止旋转,一般出现在切换加载网络音乐“等待”结束后(网络)  
/// 卡顿:不清楚是怎么回事,反正就是你切换加载音乐后立刻就会出现短暂的“静帧”,不知道是“等待”和“静帧”同时导致的,还是单纯“静帧”导致的,反正“卡顿”就是“等待”和“静帧”二合一了,一般在本地切换音乐时出现(本地)。
/// 流畅:既不会等待也不会静帧


废话不说,上方案,首先介绍下三者各自的优缺点:

1.Unity传统加载方式:
/// 能播放mp3 wav ogg 格式
/// 本地:切换加载mp3和ogg音乐会明显“卡顿”;切换加载wav音乐微不可查“卡顿”,哪怕是80兆的大文件,这会让人误以为是“流畅”,不过勉强算作“流畅”也没问题(不知道是我眼花了还是真的会有微不可察的卡顿)
/// 网络:切换加载所有音乐文件都需要明显长时间的“等待”(自己服务器,网不好),直到所有数据下载完才会播放;但是mp3和ogg音乐播放前会明显“静帧”,wav则是“流畅”
/// 无论读取本地音乐还是网络音乐,都是全部加载到内存再播放


2.伪-流音频加载:
/// 能播放mp3 wav ogg 格式
/// 本地:切换加载所有音乐文件都很“流畅”,哪怕是80兆的大文件
/// 网络:切换加载所有音乐文件都需要明显长时间的“等待”(自己服务器,网不好),直到所有数据下载完才会播放;所有文件都能“流畅”播放,没有“静帧”
/// 无论读取本地音乐还是网络音乐,都是全部加载到内存再播放

3.真-流音频加载:
/// 只能播放wav ogg 格式
/// 本地:切换加载所有音乐文件都很“流畅”,哪怕是80兆的大文件,边读取数据边播放
/// 
/// 网络:切换加载所有音乐文件都需要明显“等待”,这是在等待缓冲量下载,不过等待时间很短暂。(你的网和服务器的网足够快,缓冲量很小,等待就不会明显)。
/// 网络:缓冲量下载足够后,会一直“流畅”播放音频,边下载数据边播放(如果你网络实在差的要命,那当然还是会卡,具体怎么卡我没试,也没处理)
/// 
/// 无论读取本地音乐还是网络音乐,都是边下载数据边播放。如果采用全部下载再播放的传统模式播放网络音乐,那播放一个音乐就得“卡顿”或“等待”很长时间,很不好。采用了流音频,只需要“等待”零点几秒就能“流畅”播曲,也没有画面“静帧”。

下面按顺序分别是三种音频播放器代码:
(用于测试的本地和网络音频文件大家自己搞定吧,记得测试运行前把文件弄好,给公开变量重新赋值哦)
(如果本文对你有帮助,记得点赞订阅收藏评论哦,谢谢)


传统音频播放器:

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 注释必看!!!!!!!!!!!!!!!!
/// 
/// 
/// 规定几个特殊名词:
/// 等待:画面正常运行,但是正在读取音频数据,并没播放音乐,一般出现在切换加载网络音乐后(网络)             
/// 静帧:画面出现短暂静置,不刷新了,最直观的就是你放一个一直旋转的立方体,它会短暂的停止旋转,一般出现在切换加载网络音乐“等待”结束后(网络)  
/// 卡顿:不清楚是怎么回事,反正就是你切换加载音乐后立刻就会出现短暂的“静帧”,不知道是“等待”和“静帧”同时导致的,还是单纯“静帧”导致的,反正“卡顿”就是“等待”和“静帧”二合一了,一般在本地切换音乐时出现(本地)。
/// 流畅:既不会等待也不会静帧
/// 
/// 
/// [传统音频播放器]
/// 从本地或网络加载外部文件
/// Unity版本2020.3.30
/// Win10Unity编辑器
/// 能播放mp3 wav ogg 格式
/// 本地:切换加载mp3和ogg音乐会明显“卡顿”;切换加载wav音乐微不可查“卡顿”,哪怕是80兆的大文件,这会让人误以为是“流畅”,不过勉强算作“流畅”也没问题(不知道是我眼花了还是真的会有微不可察的卡顿)
/// 网络:切换加载所有音乐文件都需要明显 长时间的“等待”(自己服务器,网不好),直到所有数据下载完才会播放;但是mp3和ogg音乐播放前会明显“静帧”,wav则是“流畅”
/// 无论读取本地音乐还是网络音乐,都是全部加载到内存再播放
/// 
/// 以上所说的问题,只针对中大型文件,如果你的文件特别小,那恐怕是察觉不到的
/// 
///  最讨人嫌的其实就是“静帧”,这本质上是一帧内处理了太多东西导致主线程卡死,你能想象玩游戏时一播放音效和背景音乐就卡一下嘛,“等待”其实反而无所谓,因为它从画面上是没有体现的。
///  
/// 具体细节请自行测试,光靠文字描述恐怕还是不能理解各种状况
/// </summary>
public class TraditionalAudioPlayer : MonoBehaviour
{


    /// <summary>
    /// 网络请求
    /// </summary>
    UnityWebRequest audioWebRequest = null;

    /// <summary>
    /// 音乐播放组件
    /// </summary>
    AudioSource audioPlayer;

    /// <summary>
    /// 当前音乐片段
    /// </summary>
    AudioClip currentAudioClip;

    /// <summary>
    /// 音乐文件名数组
    /// </summary>
    public List<string> audioUrls;

    /// <summary>
    /// 当前音频路径
    /// </summary>
    public string url;

    /// <summary>
    /// 当前音频索引
    /// </summary>
    public int audioIndex;

    /// <summary>
    /// 是否正在加载数据
    /// </summary>
    public bool isLoad = false;


    /// <summary>
    /// 是否人为中止
    /// </summary>
    public bool cancel = false;


    private void Awake()
    {
        audioPlayer = GetComponent<AudioSource>();
    }


    private void Start()
    {
        //一秒后自动播放音乐
        Invoke("Init", 1.0f);
    }



    void Init()
    {
        if (audioIndex < 0)
        {
            audioIndex = 0;
        }
        PlayMusic(audioUrls[audioIndex]);
    }


    private async void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            if (audioIndex ==0)
            {
                print("已经是第一首");
            }
            else
            {
                print("播放上一首");
                audioIndex -= 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }         
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            if (audioIndex == audioUrls.Count-1)
            {
                print("已经是最后一首");
            }
            else
            {
                print("播放下一首");
                audioIndex += 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }          
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            if (audioPlayer != null && audioPlayer.clip != null)
            {
                if (audioPlayer.isPlaying)
                {
                    print("音乐暂停");
                    audioPlayer.Pause();
                }
                else
                {
                    print("音乐恢复播放");
                    audioPlayer.Play();
                }
            }
        }
    }


    /// <summary>
    /// 播放音乐
    /// </summary>
    public void PlayMusic(string url)
    {
        print("命令播放新音乐");
        this.url = url;
        StartCoroutine("IELoadExternalAudioWebRequest");
    }


    /// <summary>
    /// 停止音乐
    /// </summary>
    public async Task StopMusic()
    {
        print("停止播放音乐");
        //正在加载的话,需要中断加载并等待中断结束
        if (isLoad)
        {
            print("检测到正在加载数据,停止加载!");
            //人为中止网络加载
            cancel = true;
            audioWebRequest.Abort();
            print("已执行停止操作");
            await WaitCancelEnd();
            ClearAsset();
            print("停止操作已结束,加载数据过程彻底结束,资源已清空!");
        }
        //没有处于加载过程,直接清除资源即可
        else
        {
            ClearAsset();
            print("停止操作已结束,资源已清空!");
        }
    }


    /// <summary>
    /// 等待取消操作结束
    /// 异步线程中等待isLoad变成false,协程函数彻底运行结束后自动变成false
    /// </summary>
    /// <returns></returns>
    Task WaitCancelEnd()
    {
        return Task.Run(() =>
        {
            while (cancel)
            {
                Thread.Sleep(1);
            }
        });
    }


    /// <summary>
    /// 协程加载外部音频---传统普通版
    /// </summary>
    /// <returns></returns>
    private IEnumerator IELoadExternalAudioWebRequest()
    {
        //如果是安卓,需要加前缀
        //这个是对安卓本地的,安卓加载网络音乐是否需要前缀不清楚,没测试
        if (Application.platform == RuntimePlatform.Android)
        {
            url = "jar:file://" + url;
        }
        isLoad = true;
        cancel = false;

        //音乐播放格式,设置为UNKNOWN,让其自行检测格式,兼容性更好
        using (audioWebRequest = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.UNKNOWN))
        {
            //三个不重要的东西,大致意思是结束后自动释放资源
            audioWebRequest.disposeCertificateHandlerOnDispose = true;
            audioWebRequest.disposeDownloadHandlerOnDispose = true;
            audioWebRequest.disposeUploadHandlerOnDispose = true;

            //无论是采取传统音频还是流音频,yield return 都是等待加载/下载全部结束
            yield return audioWebRequest.SendWebRequest();

            print("数据加载中,请耐心等待!!!!!!");

            //网络请求数据加载全部结束或者此过程人为中止、网络出错后,简言之就是网络过程结束,此值会为真。
            //我们这里是等待数据全部加载/下载的,没有全部完毕,就什么都不做,所以不必考虑此值为假的状况。
            if (audioWebRequest.isDone)
            {
                //人为中止或网络出错
                if (audioWebRequest.result == UnityWebRequest.Result.ProtocolError || audioWebRequest.result == UnityWebRequest.Result.ConnectionError)
                {
                    //人为中止,这是我们的自定义中止标志
                    if (cancel)
                    {
                        print($"人为中断了数据加载,数据下载被终止!");
                    }
                    //网络出错
                    else
                    {
                        print($"播放外部音频 {url} 时出错!");
                    }
                }
                //正常结束,也即是数据加载完全结束
                else
                {
                    //通过静态函数获取音频片段 DownloadHandlerAudioClip.GetContent和downloadHandlerAudioClip.audioClip看似一样,实则不同。
                    //前者是要求数据必须下载完,后者支持边下边播流音频
                    currentAudioClip = DownloadHandlerAudioClip.GetContent(audioWebRequest);
                    audioPlayer.clip = currentAudioClip;
                    audioPlayer.Play();
                    print("音频成功加载,开始播放");
                }
            }
        }
        isLoad = false;
        cancel = false;
        print("重置音乐加载标志");
    }





    /// <summary>
    /// 清除音频资源
    /// </summary>
    void ClearAsset()
    {
        //先停止音频播放并将音频片段和音频播放器解绑
        if (audioPlayer != null)
        {
            if (audioPlayer.isPlaying)
            {
                audioPlayer.Stop();
            }
            if (audioPlayer.clip != null)
            {
                audioPlayer.clip = null;
            }
        }
        // 在此步骤之前,确保该音频片段只有currentAudioClip一个变量在引用,否则会有内存泄漏的风险
        if (currentAudioClip != null)
        {
            //卸载音频真实数据
            currentAudioClip.UnloadAudioData();
            //销毁currentAudioClip数据对象
            Destroy(currentAudioClip);
            //变量置空
            currentAudioClip = null;
        }
    }


    private void OnDestroy()
    {
        ClearAsset();
    }

}







伪-流音频播放器:
 

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 注释必看!!!!!!!!!!!!!!!!
/// 
/// 
/// 规定几个特殊名词:
/// 等待:画面正常运行,但是正在读取音频数据,并没播放音乐,一般出现在切换加载网络音乐后(网络)             
/// 静帧:画面出现短暂静置,不刷新了,最直观的就是你放一个一直旋转的立方体,它会短暂的停止旋转,一般出现在切换加载网络音乐“等待”结束后(网络)  
/// 卡顿:不清楚是怎么回事,反正就是你切换加载音乐后立刻就会出现短暂的“静帧”,不知道是“等待”和“静帧”同时导致的,还是单纯“静帧”导致的,反正“卡顿”就是“等待”和“静帧”二合一了,一般在本地切换音乐时出现(本地)。
/// 流畅:既不会等待也不会静帧
/// 
/// 
/// [伪-流音频播放器]
/// 从本地或网络加载外部文件
/// Unity版本2020.3.30
/// Win10Unity编辑器
/// 能播放mp3 wav ogg 格式
/// 本地:切换加载所有音乐文件都很“流畅”,哪怕是80兆的大文件
/// 网络:切换加载所有音乐文件都需要明显 长时间的“等待”(自己服务器,网不好),直到所有数据下载完才会播放;所有文件都能“流畅”播放,没有“静帧”
/// 无论读取本地音乐还是网络音乐,都是全部加载到内存再播放
/// 
/// 以上所说的问题,只针对中大型文件,如果你的文件特别小,那恐怕是察觉不到的
/// 
///  最讨人嫌的其实就是“静帧”,这本质上是一帧内处理了太多东西导致主线程卡死,你能想象玩游戏时一播放音效和背景音乐就卡一下嘛,“等待”其实反而无所谓,因为它从画面上是没有体现的。
///  
/// 具体细节请自行测试,光靠文字描述恐怕还是不能理解各种状况
/// </summary>
public class PseudoStreamingAudioPlayer : MonoBehaviour
{


    /// <summary>
    /// 网络请求
    /// </summary>
    UnityWebRequest audioWebRequest = null;

    /// <summary>
    /// 音乐播放组件
    /// </summary>
    AudioSource audioPlayer;

    /// <summary>
    /// 当前音乐片段
    /// </summary>
    AudioClip currentAudioClip;

    /// <summary>
    /// 音乐文件名数组
    /// </summary>
    public List<string> audioUrls;

    /// <summary>
    /// 当前音频路径
    /// </summary>
    public string url;

    /// <summary>
    /// 当前音频索引
    /// </summary>
    public int audioIndex;

    /// <summary>
    /// 是否正在加载数据
    /// </summary>
    public bool isLoad = false;


    /// <summary>
    /// 是否人为中止
    /// </summary>
    public bool cancel = false;


    private void Awake()
    {
        audioPlayer = GetComponent<AudioSource>();
    }


    private void Start()
    {
        //一秒后自动播放音乐
        Invoke("Init", 1.0f);
    }



    void Init()
    {
        if (audioIndex < 0)
        {
            audioIndex = 0;
        }
        PlayMusic(audioUrls[audioIndex]);
    }


    private async void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            if (audioIndex == 0)
            {
                print("已经是第一首");
            }
            else
            {
                print("播放上一首");
                audioIndex -= 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            if (audioIndex == audioUrls.Count - 1)
            {
                print("已经是最后一首");
            }
            else
            {
                print("播放下一首");
                audioIndex += 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            if (audioPlayer != null && audioPlayer.clip != null)
            {
                if (audioPlayer.isPlaying)
                {
                    print("音乐暂停");
                    audioPlayer.Pause();
                }
                else
                {
                    print("音乐恢复播放");
                    audioPlayer.Play();
                }
            }
        }
    }


    /// <summary>
    /// 播放音乐
    /// </summary>
    public void PlayMusic(string url)
    {
        print("命令播放新音乐");
        this.url = url;
        StartCoroutine("IELoadExternalAudioWebRequest");
    }


    /// <summary>
    /// 停止音乐
    /// </summary>
    public async Task StopMusic()
    {
        print("停止播放音乐");
        //正在加载的话,需要中断加载并等待中断结束
        if (isLoad)
        {
            print("检测到正在加载数据,停止加载!");
            //人为中止网络加载
            cancel = true;
            audioWebRequest.Abort();
            print("已执行停止操作");
            await WaitCancelEnd();
            ClearAsset();
            print("停止操作已结束,加载数据过程彻底结束,资源已清空!");
        }
        //没有处于加载过程,直接清除资源即可
        else
        {
            ClearAsset();
            print("停止操作已结束,资源已清空!");
        }
    }


    /// <summary>
    /// 等待取消操作结束
    /// 异步线程中等待isLoad变成false,协程函数彻底运行结束后自动变成false
    /// </summary>
    /// <returns></returns>
    Task WaitCancelEnd()
    {
        return Task.Run(() =>
        {
            while (cancel)
            {
                Thread.Sleep(1);
            }
        });
    }


    /// <summary>
    /// 协程加载外部音频---传统普通版
    /// </summary>
    /// <returns></returns>
    private IEnumerator IELoadExternalAudioWebRequest()
    {
        //如果是安卓,需要加前缀
        //这个是对安卓本地的,安卓加载网络音乐是否需要前缀不清楚,没测试
        if (Application.platform == RuntimePlatform.Android)
        {
            url = "jar:file://" + url;
        }
        isLoad = true;
        cancel = false;

        //音乐播放格式,设置为UNKNOWN,让其自行检测格式,兼容性更好
        using (audioWebRequest = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.UNKNOWN))
        {
            //三个不重要的东西,大致意思是结束后自动释放资源
            audioWebRequest.disposeCertificateHandlerOnDispose = true;
            audioWebRequest.disposeDownloadHandlerOnDispose = true;
            audioWebRequest.disposeUploadHandlerOnDispose = true;


            // 设置为流式播放(非常重要的东西,没有它流音频无从谈起,即便是伪-流音频也需要此属性)
            DownloadHandlerAudioClip downloadHandlerAudioClip = (DownloadHandlerAudioClip)audioWebRequest.downloadHandler;
            downloadHandlerAudioClip.streamAudio = true;


            //无论是采取传统音频还是流音频,yield return 都是等待加载/下载全部结束
            yield return audioWebRequest.SendWebRequest();

            print("数据加载中,请耐心等待!!!!!!");

            //网络请求数据加载全部结束或者此过程人为中止、网络出错后,简言之就是网络过程结束,此值会为真。
            //我们这里是等待数据全部加载/下载的,没有全部完毕,就什么都不做,所以不必考虑此值为假的状况。
            if (audioWebRequest.isDone)
            {
                //人为中止或网络出错
                if (audioWebRequest.result == UnityWebRequest.Result.ProtocolError || audioWebRequest.result == UnityWebRequest.Result.ConnectionError)
                {
                    //人为中止,这是我们的自定义中止标志
                    if (cancel)
                    {
                        print($"人为中断了数据加载,数据下载被终止!");
                    }
                    //网络出错
                    else
                    {
                        print($"播放外部音频 {url} 时出错!");
                    }
                }
                //正常结束,也即是数据加载完全结束
                else
                {
                    //通过静态函数获取音频片段 DownloadHandlerAudioClip.GetContent和downloadHandlerAudioClip.audioClip看似一样,实则不同。
                    //前者是要求数据必须下载完,后者支持边下边播流音频
                    currentAudioClip = DownloadHandlerAudioClip.GetContent(audioWebRequest);
                    audioPlayer.clip = currentAudioClip;
                    audioPlayer.Play();
                    print("音频成功加载,开始播放");
                }
            }
        }
        isLoad = false;
        cancel = false;
        print("重置音乐加载标志");
    }





    /// <summary>
    /// 清除音频资源
    /// </summary>
    void ClearAsset()
    {
        //先停止音频播放并将音频片段和音频播放器解绑
        if (audioPlayer != null)
        {
            if (audioPlayer.isPlaying)
            {
                audioPlayer.Stop();
            }
            if (audioPlayer.clip != null)
            {
                audioPlayer.clip = null;
            }
        }
        // 在此步骤之前,确保该音频片段只有currentAudioClip一个变量在引用,否则会有内存泄漏的风险
        if (currentAudioClip != null)
        {
            //卸载音频真实数据
            currentAudioClip.UnloadAudioData();
            //销毁currentAudioClip数据对象
            Destroy(currentAudioClip);
            //变量置空
            currentAudioClip = null;
        }
    }


    private void OnDestroy()
    {
        ClearAsset();
    }

}





真-流音频播放器:
 

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 注释必看!!!!!!!!!!!!!!!!
/// 我的个人服务器带宽很慢,有条件可以用商业服务器的音乐测试,效果更好
/// 
/// 
/// 规定几个特殊名词:
/// 等待:画面正常运行,但是正在读取音频数据,并没播放音乐,一般出现在切换加载网络音乐后(网络)             
/// 静帧:画面出现短暂静置,不刷新了,最直观的就是你放一个一直旋转的立方体,它会短暂的停止旋转,一般出现在切换加载网络音乐“等待”结束后(网络)  
/// 卡顿:不清楚是怎么回事,反正就是你切换加载音乐后立刻就会出现短暂的“静帧”,不知道是“等待”和“静帧”同时导致的,还是单纯“静帧”导致的,反正“卡顿”就是“等待”和“静帧”二合一了,一般在本地切换音乐时出现(本地)。
/// 流畅:既不会等待也不会静帧
/// 
/// 
/// [真-流音频播放器]
/// 从本地或网络加载外部文件
/// Unity版本2020.3.30
/// Win10Unity编辑器
/// 只能播放wav ogg 格式
/// 本地:切换加载所有音乐文件都很“流畅”,哪怕是80兆的大文件,边读取数据边播放
/// 
/// 网络:切换加载所有音乐文件都需要明显“等待”,这是在等待缓冲量下载,不过等待时间很短暂。(你的网和服务器的网足够快,缓冲量很小,等待就不会明显)。
/// 网络:缓冲量下载足够后,会一直“流畅”播放音频,边下载数据边播放(如果你网络实在差的要命,那当然还是会卡,具体怎么卡我没试,也没处理)
/// 
/// 无论读取本地音乐还是网络音乐,都是边下载数据边播放。如果采用全部下载再播放的传统模式播放网络音乐,那播放一个音乐就得“卡顿”或“等待”很长时间,很不好。采用了流音频,只需要“等待”零点几秒就能“流畅”播曲,也没有画面“静帧”。
/// 
/// 不要想着去播放mp3文件,会报错的。音频文件格式需要特定工具转化,偷懒仅修改后缀名是没用的。
/// 
/// 最讨人嫌的其实就是“静帧”,这本质上是一帧内处理了太多东西导致主线程卡死,你能想象玩游戏时一播放音效和背景音乐就卡一下嘛,“等待”其实反而无所谓,因为它从画面上是没有体现的。
///  
/// 具体细节请自行测试,光靠文字描述恐怕还是不能理解各种状况
/// </summary>
public class TrueStreamingAudioPlayer : MonoBehaviour
{
    /// <summary>
    /// 网络请求
    /// </summary>
    UnityWebRequest audioWebRequest = null;

    /// <summary>
    /// 音乐播放组件
    /// </summary>
    AudioSource audioPlayer;

    /// <summary>
    /// 当前音乐片段
    /// </summary>
    AudioClip currentAudioClip;

    /// <summary>
    /// 音乐文件名数组
    /// </summary>
    public List<string> audioUrls;

    /// <summary>
    /// 当前音频路径
    /// </summary>
    public string url;

    /// <summary>
    /// 当前音频索引
    /// </summary>
    public int audioIndex;

    /// <summary>
    /// 是否正在加载数据
    /// </summary>
    public bool isLoad = false;

    /// <summary>
    /// 是否人为中止
    /// </summary>
    public bool cancel = false;


    /// <summary>
    /// 音频数据请求的最小数据量
    /// 可以根据你的硬件配置或网络速度自行配置
    /// 单位为字节byte
    /// </summary>
    public ulong bufferBytes = 262144;


    private void Awake()
    {
        audioPlayer = GetComponent<AudioSource>();
    }


    private void Start()
    {
        //一秒后自动播放音乐
        Invoke("Init", 1.0f);
    }


    void Init()
    {
        if (audioIndex < 0)
        {
            audioIndex = 0;
        }
        PlayMusic(audioUrls[audioIndex]);
    }


    private async void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            if (audioIndex == 0)
            {
                print("已经是第一首");
            }
            else
            {
                print("播放上一首");
                audioIndex -= 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            if (audioIndex == audioUrls.Count - 1)
            {
                print("已经是最后一首");
            }
            else
            {
                print("播放下一首");
                audioIndex += 1;
                //异步函数等待,等待停止过程完成,一般只有几毫秒,不会因此造成卡顿,但又不能不等,毕竟彻底停止还是需要点时间的
                await StopMusic();
                PlayMusic(audioUrls[audioIndex]);
            }
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            if (audioPlayer != null&& audioPlayer.clip!=null)
            {
                if (audioPlayer.isPlaying)
                {
                    print("音乐暂停");
                    audioPlayer.Pause();
                }
                else
                {
                    print("音乐恢复播放");
                    audioPlayer.Play();
                }
            }
        }
    }


    /// <summary>
    /// 播放音乐
    /// </summary>
    public void PlayMusic(string url)
    {
        print("命令播放新音乐");
        this.url = url;
        StartCoroutine("TrueStreamingAudio_IELoadExternalAudioWebRequest");
    }


    /// <summary>
    /// 停止音乐
    /// </summary>
    public async Task StopMusic()
    {
        print("停止播放音乐");
        //正在加载的话,需要中断加载并等待中断结束
        if (isLoad)
        {
            print("检测到正在加载数据,停止加载!");
            //人为中止网络加载
            cancel = true;
            audioWebRequest.Abort();
            print("已执行停止操作");
            await WaitCancelEnd();
            ClearAsset();
            print("停止操作已结束,加载数据过程彻底结束,资源已清空!");
        }
        //没有处于加载过程,直接清除资源即可
        else
        {
            ClearAsset();
            print("停止操作已结束,资源已清空!");
        }
    }


    /// <summary>
    /// 等待取消操作结束
    /// 异步线程中等待isLoad变成false,协程函数彻底运行结束后自动变成false
    /// </summary>
    /// <returns></returns>
    Task WaitCancelEnd()
    {
        return Task.Run(() =>
        {
            while (cancel)
            {
                Thread.Sleep(1);
            }
        });
    }




    /// <summary>
    ///  协程加载外部音频---真流音频
    /// </summary>
    /// <returns></returns>
    IEnumerator TrueStreamingAudio_IELoadExternalAudioWebRequest()
    {
        //如果是安卓,需要加前缀
        //这个是对安卓本地的,安卓加载网络音乐是否需要前缀不清楚,没测试
        if (Application.platform == RuntimePlatform.Android)
        {
            url = "jar:file://" + url;
        }
        isLoad = true;
        cancel = false;

        //音乐播放格式,设置为UNKNOWN,让其自行检测格式,兼容性更好
        using (audioWebRequest = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.UNKNOWN))
        {
            //三个不重要的东西,大致意思是结束后自动释放资源
            audioWebRequest.disposeCertificateHandlerOnDispose = true;
            audioWebRequest.disposeDownloadHandlerOnDispose = true;
            audioWebRequest.disposeUploadHandlerOnDispose = true;

            // 设置为流式播放(非常重要的东西,没有它流音频无从谈起)
            DownloadHandlerAudioClip downloadHandlerAudioClip = (DownloadHandlerAudioClip)audioWebRequest.downloadHandler;
            downloadHandlerAudioClip.streamAudio = true;

            //无论是采取传统音频还是流音频,yield return 都是等待加载/下载全部结束,所以我们这里不用yield return
            audioWebRequest.SendWebRequest();

            print("数据加载中,请耐心等待!!!!!!");

            //长度一定为0,刚发请求哪来的数据嘛
            print($"发送网络请求后立刻查看数据量长度----数据下载进度:{audioWebRequest.downloadProgress}   数据下载长度:{audioWebRequest.downloadedBytes}");
            
            //循环判断
            while (true)
            {
                //网络请求数据加载全部结束或者此过程人为中止、网络出错后,简言之就是网络过程结束,此值会为真。
                //我们这里是流音频边下边播,如果数据没有全部结束,且没有出错,处于正在下载数据状态,那就需要去此值为假中进行判断
                //网络过程结束
                if (audioWebRequest.isDone)
                {
                    //人为中止或网络出错
                    if (audioWebRequest.result == UnityWebRequest.Result.ProtocolError || audioWebRequest.result == UnityWebRequest.Result.ConnectionError)
                    {
                        //人为中止,这是我们的自定义中止标志
                        if (cancel)
                        {
                            print($"人为中断了数据加载,数据下载被终止!");
                        }
                        //网络出错
                        else
                        {
                            print($"播放外部音频 {url} 时出错!");
                        }

                    }
                    //正常结束
                    else
                    {
                        //如果数据正常加载完毕且当前音频片段为空,就大概率是从本地加载数据且速度太快了,一帧就加载完了,没能进到缓冲量判断里面去
                        //所以这里需要补一下,把有可能漏空的数据赋值
                        if (currentAudioClip == null)
                        {
                            //通过实例化获取音频片段    DownloadHandlerAudioClip.GetContent和downloadHandlerAudioClip.audioClip看似一样,实则不同。
                            //前者是要求数据必须下载完,后者支持边下边播流音频
                            currentAudioClip = downloadHandlerAudioClip.audioClip;
                            audioPlayer.clip = currentAudioClip;
                            audioPlayer.Play();
                        }
                        print("正常结束下载,所有音频数据下载完毕!");
                        print($"数据下载进度:{audioWebRequest.downloadProgress}   数据下载长度:{audioWebRequest.downloadedBytes}");
                    }
                    break;
                }
                //正在正常的下载网络数据
                else
                {
                    //先判断缓冲量下载
                    //判断已加载的音频数据是否大于缓冲数据,否则一点数据没有如何播放
                    //如果网络号,缓冲量可以设置的足够小,一般就一两帧便能下载够足够的缓冲数据,这就是为什么不卡顿的原因
                    //边下边播,只要下载速度大于播放速度就没问题
                    //缓冲量足够,就要为音频播放器设置audioclip
                    if (audioWebRequest.downloadedBytes > bufferBytes)
                    {
                        //如果缓冲量足够且当前音频片段为空,就给他设置新的音频片段
                        if (currentAudioClip == null)
                        {
                            //通过实例化获取音频片段    DownloadHandlerAudioClip.GetContent和downloadHandlerAudioClip.audioClip看似一样,实则不同。
                            //前者是要求数据必须下载完,后者支持边下边播流音频
                            currentAudioClip = downloadHandlerAudioClip.audioClip;
                            audioPlayer.clip = currentAudioClip;
                            audioPlayer.Play();
                            print("获取到最小缓冲量,音频成功加载,开始播放!");                          
                        }
                        //等待下载剩余的音频数据
                        print($"数据下载进度:{audioWebRequest.downloadProgress}   数据下载长度:{audioWebRequest.downloadedBytes}");
                        yield return null;
                    }
                    //缓冲量不足继续缓冲
                    else
                    {
                        //如果在这里检查到取消下载标志为真,这意味着连最小缓冲量都没满足,网络就被人为中止了,只可能发生在网络特别差或者缓冲量设置的特别大的情况下
                        if (cancel)
                        {
                            print("连最小缓冲量还未满足就结束了数据加载!");
                        }
                        else
                        {
                            // 等待缓冲更多的数据,否则数据太少没法播放,等待一帧后再判断下载进度
                            print($"数据下载进度:{audioWebRequest.downloadProgress}   数据下载长度:{audioWebRequest.downloadedBytes}");
                            yield return null;
                        }
                    }
                }
            }

        }
        isLoad = false;
        cancel = false;
        print("重置音乐播放标志");
    }





    /// <summary>
    /// 清除音频资源
    /// </summary>
    void ClearAsset()
    {
        //先停止音频播放并将音频片段和音频播放器解绑
        if (audioPlayer != null)
        {
            if (audioPlayer.isPlaying)
            {
                audioPlayer.Stop();
            }
            if (audioPlayer.clip != null)
            {
                audioPlayer.clip = null;
            }
        }
        // 在此步骤之前,确保该音频片段只有currentAudioClip一个变量在引用,否则会有内存泄漏的风险
        if (currentAudioClip != null)
        {
            //卸载音频真实数据
            currentAudioClip.UnloadAudioData();
            //销毁currentAudioClip数据对象
            Destroy(currentAudioClip);
            //变量置空
            currentAudioClip = null;
        }
    }


    private void OnDestroy()
    {
        ClearAsset();
    }


}



 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Unity 音频卡顿 静帧 等待等问题的解决方案 的相关文章

  • 复杂曲面建模_Rhino 7

    Rhino 和 Sketchup之间的 斗争 已经持续了好长时间 双方基本态度是 呵 你很好用 但我也可以啊 这种争议的底层原因是因为他们无法互通 我们并没有听说过Rhino和Revit有矛盾 也没听说过Sketchup和AutoCAD有什
  • C++系列目录

    基础语言篇 C 数据类型 C位操作 C预编译处理 C指针 C结构体与枚举类型 C 函数 C 虚函数 C 容器与算法 C 类 C I O处理 C 重载操作符与转换 模板与泛型 C C 编译和调试 C C 动态链接 C C 通用MakeFile

随机推荐

  • element表格默认选中

    场景 选中表格多选框后 重新返回这个标签页 已经选择的需要默认选中 但是重新返回后 并没有选中
  • 流计算框架 Flink 与 Storm 的性能对比

    概述 将分布式实时计算框架 Flink 与 Storm 进行性能对比 为实时计算平台和业务提供数据参考 一 背景 Apache Flink 和 Apache Storm 是当前业界广泛使用的两个分布式实时计算框架 其中 Apache Sto
  • 04-分布式资源管理系统YARN

    目录 一 YARN简介 1 YARN的由来 2 什么是YARN 二 YARN原理 1 系统架构 1 1 角色分工 1 2 设计思想 1 3 工作机制 1 4 集群部署 2 YARN高可用 三 YARN资源调度策略 1 FIFO调度器 2 容
  • Android智能下拉刷新框架—SmartRefreshLayout的使用

    转载请注明出处 http blog csdn net jarchie520 article details 78193387 上个月因为自己太懒了 加上又发生了一点小事 就没能及时更新博客 下了班回家面壁思过去吧 今天这篇文章主要是介绍一下
  • 帮程序员减压放松的10个良心网站

    同学们工作之余 不妨放下微博跟朋友圈 来这10个网站感受一下看着就醉了的情境 念完往上一推音乐键 我往后一靠 潮乎乎的软皮耳机里头 音乐排山倒海 今天推荐的网站 利用代入感强的图片与音频 迅速帮你抹平焦虑 获得平和心态 特别献需求改千遍的程
  • LeetCode-3. 无重复字符的最长子串 -- Python解

    原题描述 给定一个字符串 s 请你找出其中不含有重复字符的 最长子串 的长度 示例 1 输入 s abcabcbb 输出 3 解释 因为无重复字符的最长子串是 abc 所以其长度为 3 示例 2 输入 s bbbbb 输出 1 解释 因为无
  • 2011年中的macmini 系统安装,简直作死

    不想再爱mac了 再不要爱了 完结 这几天真的时间就耗在这系统上了 之前一直用的是win10系统 直接把苹果系统整个的推掉了 由于是真的不知道能直接U盘装10 13版本 索性理所当然的直接一步到位到10 14最新版 所以花了半天时间找镜像d
  • HTML中Form表单的使用

    1 form表单标记 表单标记以
  • 金融圈:Hoping Club华英会将重金注资收购REVA

    近期 金融圈有消息传出 华英会或将注资收购REVA提高其所持有的股份 来获取REVA中国大陆区ArtStreet质押平台的运营权 这一消息受到了很多业内人士的关注 一旦此次收购坐实成功也就意味着华英会将获得 中国大陆REVA质押平台的运营权
  • Mybatis-plusMybatis 通过获取sqlSession执行原生sql(执行程序代码中sql字符串)

    Mybatis plus Mybatis通过获取sqlSession执行原生jdbc执行sql 此处demo只写了执行查询sql 有需要可以执行增删改查都可 与原生jdbc调用方式一样 Component Slf4j public clas
  • 178、锐捷交换机恢复出厂和各种基本配置

    锐捷最详细的基础命令 一 锐捷交换机配置原理 我们来看下锐捷的日常配置命令原理 1 进入特权模式 Ruijie gt enable 进入特权模式 2 查看设备flash当前文件列表 Ruijie dir 查看flash当前文件列表 3 将配
  • faster RCNN 的细节理解

    1 anchors不同的大小但是采用了ROI pooling一样的策略 都映射到3 3的卷积核上 最后通过1 1的卷积核 相当与全连接分成了18类 9个anchors的话 2 分类的时候 reshape 两次 第一次为了softmax分类
  • Go的并发的退出

    有时候我们需要通知goroutine停止它正在干的事情 比如一个正在执行计算的web服务 然而它的客户端已经断开了和服务端的连接 Go语言并没有提供在一个goroutine中终止另一个goroutine的方法 由于这样会导致goroutin
  • #452. 序列操作

    序列操作 题目 Daimayuan Online Judge 问题描述 思路 首先想的是第二次操作的y可以将前面所以操作进行抵消 只需要第二次操作的最大值即可 但是发现 对于第一个操作 它是单点修改 每修改一次对于第二次操作都是有影响的 导
  • 最简单的区块链实现,不到50行代码!(一)

    什么是区块链 Blockchain 一个电子记账本 以比特币和其他密码加密货币进行的交易公开地 按照日期顺序记录其中 总的来说 它是一个公开的数据库 新的数据存储在一个称为区块的容器中 并且附加到一个 不可变 的链条 即区块链 上 链条上还
  • 《基于Python的大数据分析基础及实战》第二章

    第二章 个人信息 kwd info kwd info kwd info ipynb等文件下载 https wwm lanzouf com iklXf023qeef 对数据进行分析首先得对数据进行处理 本章主要介绍P thon在数据处理方面的
  • 5.2 主机扫描:主机探测

    目录 一 预备知识 主机扫描方法 二 实验环境 三 实验步骤 一 预备知识 主机扫描方法 主机扫描 Host Scan 是指通过对目标网络 一般为一个或多个IP网段 中主机IP地址的扫描 以确定目标网络中有哪些主机处于运行状态 主机扫描的实
  • Ubuntu下,Java中利用JNI调用codeblocks c++生成的动态库的使用步骤

    1 打开新立得包管理器 搜索JDK 选择openjdk 6 jdk安装 2 打开Ubuntu软件中心 搜索Eclipse 选择Eclipse集成开发环境 安装 3 打开Eclipse File gt New gt Java Project
  • 剑指Offer 22. 链表中倒数第k个节点(Easy)/ 19. 删除链表的倒数第 N 个结点(Medium)/ ListNode调用!!!

    LeetCode 19 删除链表的倒数第 N 个结点 Medium 题目链接 题解 链表中倒数第 k 个节点 双指针 清晰图解 思路 代码 Definition for singly linked list class ListNode d
  • Unity 音频卡顿 静帧 等待等问题的解决方案

    是否遇到过在Unity中加载音频文件卡顿 也就是画面卡住 的现象 特别是加载外部音频文件时 虽然时间很短 但这终归不是什么好现象 尤其是打游戏的话 影响很大 但是一些有牌面的Boss也不能不配音乐 当然也可以通过其它方式解决 比如特定条件统