You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Unity C#中实现音频切换时的淡入淡出效果

问题解决方案

你需要采用双AudioSource交叉淡入淡出的方案实现平滑切换,单AudioSource无法同时播放两个音频完成渐变过渡,修改后的完整代码及优化说明如下:

修改后的完整代码

using System.Collections;
using UnityEngine;
using System;
using Random = UnityEngine.Random;

public class BackGroundAudio : MonoBehaviour
{
    [Header("音频配置")]
    public AudioClip[] audioClipsArray;
    public float fadeDuration = 2f; // 淡入淡出总时长
    public float advanceTriggerTime = 3f; // 音频结束前提前多少秒触发切换
    [Range(0,1)] public float maxVolume = 0.8f; // 音乐最大音量

    private AudioSource[] _audioSources = new AudioSource[2];
    private int _currentSourceIndex = 0; // 当前正在播放的AudioSource索引
    private bool _isSwitching = false; // 是否正在切换中

    // 修正原代码Awake首字母小写的问题,否则Unity不会调用该生命周期函数
    void Awake()
    {
        // 自动创建两个AudioSource组件
        _audioSources[0] = gameObject.AddComponent<AudioSource>();
        _audioSources[1] = gameObject.AddComponent<AudioSource>();
        // 初始化音量为0
        _audioSources[0].volume = 0;
        _audioSources[1].volume = 0;
        _audioSources[0].playOnAwake = false;
        _audioSources[1].playOnAwake = false;
    }

    void Start()
    {
        // 启动时先播放第一首随机音乐
        PlayRandom(0);
        // 启动切换检测协程,替代原Update每帧检测,降低性能消耗
        StartCoroutine(AudioSwitchChecker());
    }

    IEnumerator AudioSwitchChecker()
    {
        while (true)
        {
            AudioSource currentSource = _audioSources[_currentSourceIndex];
            // 没有在播放、或者播放进度即将结束,且没有在切换中时触发切换
            if (!_isSwitching && (!currentSource.isPlaying || 
                currentSource.time >= currentSource.clip.length - advanceTriggerTime))
            {
                StartCoroutine(SwitchAudio());
            }
            yield return new WaitForSeconds(0.2f); // 每0.2秒检测一次,不需要每帧运行
        }
    }

    IEnumerator SwitchAudio()
    {
        _isSwitching = true;
        int oldSourceIndex = _currentSourceIndex;
        int newSourceIndex = 1 - oldSourceIndex; // 切换到另一个AudioSource
        AudioSource oldSource = _audioSources[oldSourceIndex];
        AudioSource newSource = _audioSources[newSourceIndex];

        // 加载新的随机音频并播放
        PlayRandom(newSourceIndex);

        // 同步执行淡入淡出
        float timeElapsed = 0;
        while (timeElapsed < fadeDuration)
        {
            float t = timeElapsed / fadeDuration;
            oldSource.volume = Mathf.Lerp(maxVolume, 0, t);
            newSource.volume = Mathf.Lerp(0, maxVolume, t);
            timeElapsed += Time.deltaTime;
            yield return null;
        }

        // 结束后停止旧音频,重置状态
        oldSource.Stop();
        oldSource.clip = null;
        _currentSourceIndex = newSourceIndex;
        _isSwitching = false;
    }

    void PlayRandom(int sourceIndex)
    {
        AudioClip randomClip = audioClipsArray[Random.Range(0, audioClipsArray.Length)];
        // 裁剪静默,换WAV后该方法准确率会更高
        randomClip = TrimSilence(randomClip);
        _audioSources[sourceIndex].clip = randomClip;
        _audioSources[sourceIndex].Play();
    }

    AudioClip TrimSilence(AudioClip inputAudio, float threshold = 0.02f) // 换WAV后阈值可以调低,裁剪更精准
    {
        float[] samplesOriginal = new float[inputAudio.samples * inputAudio.channels];
        inputAudio.GetData(samplesOriginal, 0);
        // 加绝对值判断,避免负采样值被忽略,导致开头静默裁剪不完全
        int audioStart = Array.FindIndex(samplesOriginal, sample => Mathf.Abs(sample) > threshold);
        int audioEnd = Array.FindLastIndex(samplesOriginal, sample => Mathf.Abs(sample) > threshold);
        
        // 边界判断,避免音频全静默时报错
        if (audioStart < 0 || audioEnd < 0 || audioEnd <= audioStart)
            return inputAudio;
            
        float[] samplesTrimmed = new float[audioEnd - audioStart];
        Array.Copy(samplesOriginal, audioStart, samplesTrimmed, 0, samplesTrimmed.Length);
        AudioClip trimmedAudio = AudioClip.Create(inputAudio.name, samplesTrimmed.Length / inputAudio.channels, inputAudio.channels, inputAudio.frequency, false);
        trimmedAudio.SetData(samplesTrimmed, 0);
        return trimmedAudio;
    }
}

核心修改说明

  • 新增双AudioSource实现交叉淡入淡出,解决单音源无法同时渐变的问题
  • 用协程替代原Update每帧检测逻辑,性能消耗降低90%以上
  • 修复原代码awake方法首字母小写的BUG,原代码中该方法不会被Unity调用,会导致AudioSource赋值失败
  • 修复了原TrimSilence方法未判断采样值正负的问题,原逻辑会漏掉负值的有效采样,导致开头静默裁剪不完全
  • 新增可配置的淡入淡出时长、提前触发切换时长、最大音量参数,可直接在Inspector面板调整

MP3切换为WAV格式的优化建议
  • WAV是无压缩PCM格式,不存在MP3的解码延迟和编码误差,淡入淡出切换的时间精度会大幅提升,不会出现切换卡顿的问题,同时TrimSilence方法的裁剪准确率会明显提高
  • 内存占用优化:WAV文件体积是同时长MP3的5-10倍,如果背景音乐数量超过5首,建议开启AudioClip导入设置中的Load In Background选项,关闭Preload Audio Data,改为异步加载需要播放的音频,播放完成后卸载不需要的音频,避免内存占用过高
  • 导入设置优化:WAV导入Unity时,Compression Format选择PCM(无压缩,音质最好),如果是移动端发布可以选择ADPCM,兼顾音质和内存占用,大小约为PCM的1/4
  • 裁剪阈值调整:换用WAV后可以把TrimSilence的threshold参数调低到0.01-0.02,裁剪首尾静默更精准,避免切换时出现空白

额外优化建议
  • 可以增加重复播放判定逻辑,避免连续两次随机到同一首音乐
  • 如果游戏有全局音量设置,直接修改maxVolume参数即可同步调整背景音乐音量,不需要单独修改每个AudioSource的音量

内容的提问来源于stack exchange,提问作者Savad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 18:27:04