> 文章列表 > Unity 音频卡顿 静帧 等待等问题的解决方案

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

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

是否遇到过在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 returnaudioWebRequest.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{//先判断缓冲量下载//判断已加载的音频数据是否大于缓冲数据,否则一点数据没有如何播放//如果网络号,缓冲量可以设置的足够小,一般就一两帧便能下载够足够的缓冲数据,这就是为什么不卡顿的原因//边下边播,只要下载速度大于播放速度就没问题//缓冲量足够,就要为音频播放器设置audioclipif (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();}}