解决重复切换State的时候会导致重复播放对应Segment的问题。

修复FadeIn读取了FadeOut参数的问题。
增加Initial Delay功能。
重构AudioScheduler.ConfigureSource() -> SetupSource(), RegisterActiveSound(), StartPlayBack()。
移动长音频相关功能至LongAudio文件夹。
This commit is contained in:
2026-03-25 17:01:38 +08:00
parent e46e57d580
commit b6393449c9
19 changed files with 307 additions and 74 deletions
Binary file not shown.
+94 -1
View File
@@ -860,7 +860,7 @@ MonoBehaviour:
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 48
m_IntArgument: 11
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
@@ -1070,6 +1070,7 @@ RectTransform:
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1987857384}
- {fileID: 1394234348}
- {fileID: 1161878326}
- {fileID: 392790004}
@@ -1396,6 +1397,98 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1798358786}
m_CullTransparentMesh: 1
--- !u!1 &1987857383
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1987857384}
- component: {fileID: 1987857386}
- component: {fileID: 1987857385}
- component: {fileID: 1987857387}
m_Layer: 5
m_Name: AudioSystemStatus
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1987857384
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1987857383}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1667985901}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 200, y: 300}
m_Pivot: {x: 0.5, y: 1}
--- !u!114 &1987857385
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1987857383}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 18
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 0
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: Audio System Status
--- !u!222 &1987857386
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1987857383}
m_CullTransparentMesh: 1
--- !u!114 &1987857387
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1987857383}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 393e3f1fc2824bcf81260ccf46ce9524, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &2093584669
GameObject:
m_ObjectHideFlags: 0
@@ -11,6 +11,8 @@ namespace OCES.Audio
/// </summary>
public class AudioScheduler : MonoBehaviour
{
public UnityEngine.UI.Text audioSystemTextBox;
const int k_maxGlobalConcurrent = 128;
//记录某个 AudioObject 最近一次触发播放的时刻,用于 MinInterval 节流判断,是"上次什么时候播过"。
@@ -18,7 +20,7 @@ namespace OCES.Audio
readonly Dictionary<uint, int> m_clipConcurrentCount = new();
readonly List<ActiveSound> m_activeSounds = new();
// 复用列表,避免 TryPlay 每帧分配临时 List(原 LINQ .Where().ToList()
// 复用列表,避免 TryPlay 每帧分配临时 List
readonly List<ActiveSound> m_tempSameObject = new();
readonly List<ActiveSound> m_tempSameGroup = new();
readonly List<ActiveSound> m_tempLowerPriority = new();
@@ -31,6 +33,14 @@ namespace OCES.Audio
AudioContainerSelector m_containerSelector;
PitchStepManager m_pitchStepManager;
#if UNITY_EDITOR
void Update()
{
Editor.DebugInfoCollector.Instance.ActiveSounds = this.m_activeSounds;
Editor.DebugInfoCollector.Instance.ClipConcurrentCount = this.m_clipConcurrentCount;
}
#endif
int GetClipCount(uint id)
{
@@ -40,7 +50,7 @@ namespace OCES.Audio
void IncrementClipCount(uint id)
{
this.m_clipConcurrentCount[id] = GetClipCount(id) + 1;
//Debug.Log($"{id} count added to {GetClipCount(id)}");
Debug.Log($"{id} count added to {GetClipCount(id)}");
}
void DecrementClipCount(uint id)
@@ -167,45 +177,24 @@ namespace OCES.Audio
void PlayNewSound(AudioObject audioObject, float pitch)
{
// Blend:同时播放所有音轨,无需走后续逻辑
if (audioObject.ContainerType == ContainerType.Blend)
ActiveSound active = new()
{
for (int i = 0; i < audioObject.Name.Count; i++)
ConfigureSource(this.m_pool.AcquireAudioSource(), audioObject, pitch, clipIndex: i, registerRemove: true);
return;
// TODO BlendRanges, BlendCrossFadeType, BlendReactParam 支持
}
AudioSource source = this.m_pool.AcquireAudioSource();
// 持续播放模式(Continuous
if (audioObject.Name.Count > 1 && audioObject.ContainerPlayMode)
{
ActiveSound chainActive = new()
{
Source = source,
AudioObject = audioObject,
StartTime = Time.realtimeSinceStartupAsDouble,
Pitch = pitch,
};
this.m_activeSounds.Add(chainActive);
IncrementClipCount(audioObject.Id);
int continuousStart = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
chainActive.Coroutine =
StartCoroutine(PlayContainerContinuous(source, audioObject, chainActive, continuousStart, pitch));
return;
}
// 单次播放(步进模式)
int startIndex = audioObject.ContainerType switch
{
ContainerType.Random => this.m_containerSelector.PickShuffleIndex(audioObject),
ContainerType.Sequence => this.m_containerSelector.GetNextSequenceIndex(audioObject),
_ => 0,
AudioObject = audioObject,
Pitch = pitch,
State = ActiveSoundState.Pending,
StartTime = Time.realtimeSinceStartupAsDouble,
};
ConfigureSource(source, audioObject, pitch, startIndex, registerRemove: true);
if (audioObject.InitialDelay > 0)
{
this.m_activeSounds.Add(active);
IncrementClipCount(audioObject.Id);
active.Coroutine = StartCoroutine(PlayAfterDelay(active, audioObject, pitch));
return;
}
ExecutePlay(active);
}
/// <summary>
@@ -233,12 +222,14 @@ namespace OCES.Audio
limitRepetition);
// 配置并播放
if (!ConfigureSource(source, audioObject, pitch, index, registerRemove: false))
if (!SetupSource(source, audioObject, pitch, index))
{
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
yield break;
}
StartPlayback(source);
yield return new WaitWhile(() => source.isPlaying);
// 判断本轮是否播完
@@ -261,6 +252,20 @@ namespace OCES.Audio
}
}
}
static void StartPlayback(AudioSource source)
{
source.Play();
}
IEnumerator PlayAfterDelay(ActiveSound active, AudioObject audioObject, float pitch)
{
Debug.Log($"Delaying for {audioObject.InitialDelay} second(s).");
yield return new WaitForSeconds(audioObject.InitialDelay);
if (!this.m_activeSounds.Contains(active)) yield break;
ExecutePlay(active, isRegistered: true);
}
// ─────────────────────────────────────────────
// 工具方法
@@ -312,42 +317,121 @@ namespace OCES.Audio
_ => this.m_sfxGroup,
};
bool ConfigureSource(AudioSource source, AudioObject audioObject, float pitch, int clipIndex = 0, bool registerRemove = true)
bool SetupSource(AudioSource source, AudioObject audioObject, float pitch, int clipIndex = 0)
{
// TODO 现用现找可能会导致主线程卡死,尤其是低端机。需要配合Decompress配置优化性能。
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}");
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}"); // TODO 抽象同一资源加载接口
if (!clip)
{
Debug.LogError($"音频文件未找到:{audioObject.Name[clipIndex]}");
return false;
}
source.clip = clip;
source.loop = audioObject.LoopCount < 0;
source.clip = clip;
source.loop = audioObject.LoopCount < 0;
source.priority = audioObject.Priority;
source.outputAudioMixerGroup = GetMixerGroup(audioObject.MixingType);
source.pitch = pitch;
source.Play();
if (registerRemove)
{
IncrementClipCount(audioObject.Id);
ActiveSound active = new()
{
Source = source,
AudioObject = audioObject,
StartTime = Time.realtimeSinceStartupAsDouble,
Pitch = pitch,
};
this.m_activeSounds.Add(active);
active.Coroutine = StartCoroutine(RemoveWhenFinished(active));
}
return true;
}
void RegisterActiveSound(ActiveSound activeSound, AudioSource audioSource, bool isRegistered = false)
{
activeSound.Source = audioSource;
activeSound.State = ActiveSoundState.Playing;
activeSound.StartTime = Time.realtimeSinceStartupAsDouble;
if (!isRegistered)
{
this.m_activeSounds.Add(activeSound);
IncrementClipCount(activeSound.AudioObject.Id);
}
activeSound.Coroutine = StartCoroutine(RemoveWhenFinished(activeSound));
}
void ExecutePlay(ActiveSound active, bool isRegistered = false)
{
AudioObject audioObject = active.AudioObject;
float pitch = active.Pitch;
// =======================
// Blend(每个clip一个ActiveSound
// =======================
if (audioObject.ContainerType == ContainerType.Blend)
{
for (int i = 0; i < audioObject.Name.Count; i++)
{
AudioSource source = this.m_pool.AcquireAudioSource();
ActiveSound child = new()
{
AudioObject = audioObject,
Pitch = pitch,
State = ActiveSoundState.Playing
};
if (!SetupSource(source, audioObject, pitch, i))
{
this.m_pool.ReturnToPool(source.gameObject);
continue;
}
StartPlayback(source);
RegisterActiveSound(child, source);
}
// 删除 pending 占位
if (this.m_activeSounds.Contains(active))
{
DecrementClipCount(audioObject.Id);
this.m_activeSounds.Remove(active);
}
return;
}
AudioSource sourceSingle = this.m_pool.AcquireAudioSource();
// =======================
// Continuous
// =======================
if (audioObject.Name.Count > 1 && audioObject.ContainerPlayMode)
{
active.Source = sourceSingle;
active.State = ActiveSoundState.Playing;
this.m_activeSounds.Add(active);
IncrementClipCount(audioObject.Id);
int start = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
active.Coroutine = StartCoroutine(
PlayContainerContinuous(sourceSingle, audioObject, active, start, pitch)
);
return;
}
// =======================
// 单次播放
// =======================
int index = audioObject.ContainerType switch
{
ContainerType.Random => m_containerSelector.PickShuffleIndex(audioObject),
ContainerType.Sequence => m_containerSelector.GetNextSequenceIndex(audioObject),
_ => 0
};
if (!SetupSource(sourceSingle, audioObject, pitch, index))
{
m_pool.ReturnToPool(sourceSingle.gameObject);
return;
}
StartPlayback(sourceSingle);
RegisterActiveSound(active, sourceSingle, isRegistered);
}
IEnumerator RemoveWhenFinished(ActiveSound active)
{
if (active.AudioObject.LoopCount < 0)
@@ -372,7 +456,7 @@ namespace OCES.Audio
bool PlayAgain(ActiveSound active)
{
active.Source.Play();
StartPlayback(active.Source);
return true;
}
@@ -384,7 +468,7 @@ namespace OCES.Audio
//Debug.Log($"[StopSound] 协程已终止: {active.AudioObject.Name[0]}");
}
active.Source.Stop();
if(active.Source) active.Source.Stop();
DecrementClipCount(active.AudioObject.Id);
this.m_activeSounds.Remove(active);
this.m_pool.ReturnToPool(active.Source.gameObject);
@@ -405,5 +489,6 @@ namespace OCES.Audio
public int CurrentLoopCount;
public Coroutine Coroutine;
public float Pitch;
public ActiveSoundState State;
}
}
@@ -57,6 +57,13 @@ namespace OCES.Audio
Bass,
}
public enum ActiveSoundState
{
Pending, // 已进入调度,但还没真正播放
Playing, // 已经开始播放
Finished,
}
public interface IBinarySerializable
{
void DeSerialize(BinaryReader reader);
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 79201b50e9334e9b8ebb35186b5df5e4
timeCreated: 1774421407
@@ -15,6 +15,8 @@ namespace OCES.Audio
ContainerPlayHandle m_currentHandle;
Coroutine m_transitionCoroutine;
Coroutine m_currentFadeOutCoroutine;
Coroutine m_currentFadeInCoroutine;
uint m_currentContainerId;
@@ -60,14 +62,31 @@ namespace OCES.Audio
IEnumerator DoTransition(uint newContainerId, AmbienceTransition transition)
{
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
// 如果等待期间被切回来了,就不淡变了
if (this.m_currentFadeInCoroutine != null)
{
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
this.m_currentFadeInCoroutine = null;
}
if (this.m_currentFadeOutCoroutine != null)
{
this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine);
this.m_currentFadeOutCoroutine = null;
}
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
float outgoingVolume = this.m_fader.CurrentVolume;
this.m_coroutineHost.StartCoroutine(
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeOutBranch(outgoing, outgoingVolume, transition));
yield return this.m_coroutineHost.StartCoroutine(
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeInBranch(newContainerId, transition));
yield return this.m_currentFadeInCoroutine;
this.m_currentFadeInCoroutine = null;
}
// ─────────────────────────────────────────────
@@ -69,7 +69,7 @@ namespace OCES.Audio
/// </summary>
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, Action onContainerStarted = null)
{
if (transition?.FadeOutOffset > 0f)
if (transition?.FadeInOffset > 0f)
{
//Debug.Log($"Waiting {transition.FadeInOffset} to fade in.");
yield return new WaitForSeconds(transition.FadeInOffset);
@@ -82,10 +82,11 @@ namespace OCES.Audio
yield break;
}
float startVolume = transition?.FadeOutOffset > 0f ? 0f : 1f;
float startVolume = transition?.FadeInTime > 0f ? 0f : 1f;
StartNew(newContainerId, startVolume);
onContainerStarted?.Invoke();
if (transition?.FadeOutOffset > 0f)
if (transition?.FadeInTime > 0f)
{
yield return this.m_coroutineHost.StartCoroutine(
FadeIn(CurrentHandle, transition.FadeInTime));
@@ -14,6 +14,9 @@ namespace OCES.Audio
readonly MonoBehaviour m_coroutineHost;
readonly ChannelFader m_fader;
Coroutine m_currentFadeInCoroutine;
Coroutine m_currentFadeOutCoroutine;
// 当前正在播放的句柄
ContainerPlayHandle m_currentHandle;
uint m_currentContainerId;
@@ -79,6 +82,18 @@ namespace OCES.Audio
IEnumerator DoTransition(uint newContainerId, MusicTransition transition)
{
if (this.m_currentFadeInCoroutine != null)
{
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
this.m_currentFadeInCoroutine = null;
}
if (this.m_currentFadeOutCoroutine != null)
{
this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine);
this.m_currentFadeOutCoroutine = null;
}
// ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)──
if (transition != null && this.m_currentContainer != null)
{
@@ -87,13 +102,16 @@ namespace OCES.Audio
}
// ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ──
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
float outVol = this.m_fader.CurrentVolume;
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
// 如果等待期间被切回来了,就不淡变了
this.m_coroutineHost.StartCoroutine(
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
float outVol = this.m_fader.CurrentVolume;
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeOutBranch(outgoing, outVol, transition));
yield return this.m_coroutineHost.StartCoroutine(
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeInBranch(newContainerId, transition,
onContainerStarted: () =>
{
@@ -101,6 +119,8 @@ namespace OCES.Audio
this.m_currentContainer = this.m_containerConfig.QueryById(newContainerId);
this.m_playStartTime = AudioSettings.dspTime;
}));
yield return this.m_currentFadeInCoroutine;
this.m_currentFadeInCoroutine = null;
}
// ─────────────────────────────────────────────
@@ -18,6 +18,7 @@ namespace OCES.Audio
readonly MonoBehaviour m_coroutineHost;
UnityEngine.Audio.AudioMixerGroup m_mixerGroup;
// Sequence Step 模式的全局游标,key = containerId
readonly Dictionary<uint, int> m_sequenceStepIndex = new();
@@ -39,6 +39,10 @@ namespace OCES.Audio
LastMusicPathId = musicPathId;
LastAmbiencePathId = ambiencePathId;
#if UNITY_EDITOR
Editor.DebugInfoCollector.Instance.ActiveStates = this.m_activeStates;
#endif
}
// ─────────────────────────────────────────────