Unity创建AudioSource循环歌曲,首次循环后出现停顿问题求助
音乐循环切换的首次停顿问题排查与修复建议
问题背景
我需要实现多首歌曲的平滑循环与切换功能:
- 为每首歌创建独立的
AudioSource,所有曲目同时播放,通过调整音量切换当前可听歌曲。 - 每首歌在57.3秒时创建新的
AudioSource实现“重启”,旧AudioSource逐渐降低音量直至淡出(因曲目末尾存在超过57.3秒的淡出段)。
遇到的问题
- 首次循环结束时,歌曲会略早于预期结束,出现明显停顿,但后续所有循环均正常运行。
- 若将
AudioSource的playOnAwake设为true,而非在createNewAudioSource方法中调用Play(),则完全没有声音,不确定该现象是否与首循环停顿问题相关。
实现代码
using Unity.VisualScripting; using UnityEngine; public class MusicManager : MonoBehaviour { [SerializeField] AudioClip[] music; // 循环间隔时间,设置为57.3秒 [SerializeField] float timeToSwitchTracks; AudioSource[] musicSources; AudioSource[] lastMusicSources; // 当前选中的可听歌曲索引 int selectedMusic = 0; float timeSinceLastLoop = 0; private void Awake() { musicSources = new AudioSource[music.Length]; lastMusicSources = new AudioSource[music.Length]; if(timeToSwitchTracks <= 0) { Debug.Log("MusicManager中timeToSwitchTracks设置无效"); } createNewAudioSources(); } private void Update() { updateVolumes(); timeSinceLastLoop += Time.deltaTime; // 达到循环间隔时间,创建新的AudioSource if (timeSinceLastLoop > timeToSwitchTracks) { createNewAudioSources(); // 让新AudioSource的音量与旧的保持一致 setMusicVolume(lastMusicSources[selectedMusic].volume); } } void updateVolumes() { int sign = 1; for(int i = 0; i < musicSources.Length; i++) { // 选中曲目音量增加,其余曲目音量减少 sign = i == selectedMusic ? 1 : -1; musicSources[i].volume = musicSources[i].volume + (Time.deltaTime * sign); // 旧AudioSource逐渐静音 if (lastMusicSources[i] != null) { lastMusicSources[i].volume = lastMusicSources[i].volume - Time.deltaTime; } } } // 创建并播放新的AudioSource,将旧的存入lastMusicSources用于淡出 void createNewAudioSources() { timeSinceLastLoop = 0; for (int i = 0; i < musicSources.Length; i++) { lastMusicSources[i] = musicSources[i]; musicSources[i] = transform.AddComponent<AudioSource>(); musicSources[i].clip = music[i]; musicSources[i].volume = 0; musicSources[i].Play(); // 曲目最长不超过65秒,到时销毁AudioSource Destroy(musicSources[i], 65f); } } public void setSelectedMusic(int x) { selectedMusic = x; } public void setMusicVolume(float volume) { musicSources[selectedMusic].volume = volume; } }
我的疑问
我怀疑问题出在timeSinceLastLoop变量与首次AudioSource播放的不同步上,但日志显示循环计时是正确的。我不知道如何检查AudioSource是否在timeSinceLastLoop开始计数前就已运行,也不清楚具体的修复方案。
问题分析与修复方案
1. 首次循环停顿的根源
首次调用createNewAudioSources()是在Awake()中,此时timeSinceLastLoop被设为0,但AudioSource调用Play()后存在微小的启动延迟,而Update()中的计时从Awake()完成后开始累加,导致首次循环时,新AudioSource的播放时机比旧AudioSource的57.3秒节点略晚,旧音频已开始淡出但新音频未及时衔接,出现停顿。
2. 具体修复步骤
(1)对齐首次播放的计时
在createNewAudioSources()中,播放新AudioSource后同步计时,确保与音频播放进度一致:
void createNewAudioSources() { timeSinceLastLoop = 0; for (int i = 0; i < musicSources.Length; i++) { lastMusicSources[i] = musicSources[i]; musicSources[i] = transform.AddComponent<AudioSource>(); musicSources[i].clip = music[i]; musicSources[i].volume = 0; musicSources[i].Play(); // 首次创建时,同步timeSinceLastLoop与音频播放时间 if (lastMusicSources[i] == null) { timeSinceLastLoop = musicSources[i].time; } Destroy(musicSources[i], 65f); } }
(2)优化音量过渡逻辑
原逻辑中音量直接加减Time.deltaTime可能超出0-1范围,需限制区间,同时在旧音频音量为0时直接销毁:
void updateVolumes() { int sign = 1; for(int i = 0; i < musicSources.Length; i++) { sign = i == selectedMusic ? 1 : -1; // 限制音量在0-1之间 float newVolume = Mathf.Clamp(musicSources[i].volume + (Time.deltaTime * sign), 0f, 1f); musicSources[i].volume = newVolume; if (lastMusicSources[i] != null) { float newLastVolume = Mathf.Clamp(lastMusicSources[i].volume - Time.deltaTime, 0f, 1f); lastMusicSources[i].volume = newLastVolume; // 音量为0时销毁旧AudioSource if (newLastVolume <= 0f) { Destroy(lastMusicSources[i]); lastMusicSources[i] = null; } } } }
(3)解决playOnAwake无声音问题
设置playOnAwake = true时,音频会在组件添加时立即播放,但初始音量为0,需在创建后手动触发一次音量调整:
musicSources[i] = transform.AddComponent<AudioSource>(); musicSources[i].clip = music[i]; musicSources[i].volume = 0; musicSources[i].playOnAwake = true; // 针对选中曲目,初始化时赋予微小音量 if (i == selectedMusic) { musicSources[i].volume = Mathf.Clamp(Time.deltaTime, 0f, 1f); }
3. 额外优化建议
- 用
Mathf.Lerp实现平滑音量过渡,替代直接加减,让切换更自然:float targetVolume = i == selectedMusic ? 1f : 0f; musicSources[i].volume = Mathf.Lerp(musicSources[i].volume, targetVolume, Time.deltaTime * 2f); - 改用协程处理音量过渡和循环逻辑,减少
Update()中的频繁遍历,提升性能。
内容的提问来源于stack exchange,提问作者parakeet
相关产品推荐
相关产品推荐

