WIP: Music callback

- 头几拍会抖动一下,导致对不上拍子
This commit is contained in:
2026-04-07 10:06:02 +08:00
parent 0f204aa794
commit 49a502e647
21 changed files with 353 additions and 61 deletions
@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace OCES.Audio
{
@@ -10,7 +12,7 @@ namespace OCES.Audio
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
/// </summary>
public class MusicContainerPlayer
class MusicContainerPlayer
{
readonly MusicContainerConfig m_containerConfig;
readonly MusicSegmentConfig m_segmentConfig;
@@ -18,6 +20,12 @@ namespace OCES.Audio
readonly MonoBehaviour m_coroutineHost;
UnityEngine.Audio.AudioMixerGroup m_mixerGroup;
/// <summary>
/// 开始播放Container时触发 音乐回调系统
/// container本身,继承来的bpm,进入时刻的dspTime
/// </summary>
internal event Action<MusicContainer, float, double> OnContainerEntered;
internal event Action<MusicContainer> OnBlendError;
// Sequence Step 模式的全局游标,key = containerId
readonly Dictionary<uint, int> m_sequenceStepIndex = new();
@@ -45,7 +53,7 @@ namespace OCES.Audio
/// 开始播放指定 Container,播放完毕后调用 onFinished。
/// 返回可用于外部停止的句柄。
/// </summary>
public ContainerPlayHandle Play(uint containerId, System.Action onFinished = null)
internal ContainerPlayHandle Play(uint containerId, Action onFinished = null, float inheritedBpm = 0f)
{
MusicContainer container = this.m_containerConfig.QueryById(containerId);
if (container == null)
@@ -58,7 +66,7 @@ namespace OCES.Audio
ContainerPlayHandle handle = new();
handle.TargetVolume = 1f;
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
PlayContainerCoroutine(container, handle, onFinished));
PlayContainerCoroutine(container, handle, inheritedBpm, onFinished));
return handle;
}
@@ -93,14 +101,19 @@ namespace OCES.Audio
IEnumerator PlayContainerCoroutine(
MusicContainer container,
ContainerPlayHandle handle,
System.Action onFinished)
float inheritedBpm,
Action onFinished)
{
float effectiveBpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
Debug.Log($"[MusicContainerPlayer] OnContainerEntered firing, container={container.Id}, subscribers={OnContainerEntered != null}");
OnContainerEntered?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
int loopsCompleted = 0;
while (true)
{
yield return this.m_coroutineHost.StartCoroutine(
PlayContainerOnce(container, handle.TargetVolume, handle));
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm));
if (handle.Cancelled) yield break;
@@ -126,7 +139,7 @@ namespace OCES.Audio
/// <summary>
/// 播放一个 Container 一轮(不含循环逻辑)
/// </summary>
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
{
if (container.Segments == null || container.Segments.Count == 0)
yield break;
@@ -134,15 +147,15 @@ namespace OCES.Audio
switch (container.ContainerType)
{
case ContainerType.Blend:
yield return PlayBlend(container, volumeScale, handle);
yield return PlayBlend(container, volumeScale, handle, inheritedBpm);
break;
case ContainerType.Sequence:
yield return PlaySequence(container, volumeScale, handle);
yield return PlaySequence(container, volumeScale, handle, inheritedBpm);
break;
case ContainerType.Random:
yield return PlayRandom(container, volumeScale, handle);
yield return PlayRandom(container, volumeScale, handle, inheritedBpm);
break;
}
}
@@ -151,10 +164,21 @@ namespace OCES.Audio
// Blend(同时播放)
// ─────────────────────────────────────────────
IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
{
IEnumerable<float> tempos = container.Segments.Select(id =>
{
MusicContainer c = this.m_containerConfig.QueryById(id);
return c?.Bpm ?? 0f;
}).Where(b => b > 0f).Distinct();
if (tempos.Count() > 1)
{
OnBlendError?.Invoke(container);
}
// 同时启动所有子元素,等待全部结束
var childHandles = new List<ContainerPlayHandle>();
List<ContainerPlayHandle> childHandles = new();
bool allDone = false;
int remaining = container.Segments.Count;
@@ -166,7 +190,7 @@ namespace OCES.Audio
{
remaining--;
if (remaining <= 0) allDone = true;
});
}, inheritedBpm);
if (childHandle != null)
{
childHandles.Add(childHandle);
@@ -187,7 +211,7 @@ namespace OCES.Audio
// Sequence
// ─────────────────────────────────────────────
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
{
bool isStep = container.ContainerPlayMode;
@@ -195,7 +219,7 @@ namespace OCES.Audio
{
// Step: 每次只播一个,游标全局推进
int index = GetNextSequenceIndex(container);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm);
}
else
@@ -209,7 +233,7 @@ namespace OCES.Audio
if (i > 0 && container.StrategyParam > 0)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm);
}
}
}
@@ -218,7 +242,7 @@ namespace OCES.Audio
// Random
// ─────────────────────────────────────────────
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
{
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
@@ -226,7 +250,7 @@ namespace OCES.Audio
{
// Step Random: 随机选一个播放,算一次 loopCount
uint chosen = PickRandomChild(container);
yield return PlayChildAndWait(chosen, volumeScale, handle);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
}
else
{
@@ -244,7 +268,7 @@ namespace OCES.Audio
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(chosen, volumeScale, handle);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
}
}
}
@@ -256,10 +280,10 @@ namespace OCES.Audio
/// <summary>
/// 播放一个子元素(segment 或 container),等待其完成后返回。
/// </summary>
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle)
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle, float inheritedBpm)
{
bool done = false;
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true);
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm);
if (child != null)
parentHandle.ChildHandles.Add(child);
@@ -272,17 +296,17 @@ namespace OCES.Audio
/// <summary>
/// 启动一个子元素的播放,不等待,返回句柄。
/// </summary>
ContainerPlayHandle PlayChild(uint id, float volumeScale, System.Action onDone)
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm)
{
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone);
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone, inheritedBpm: inheritedBpm);
}
// ─────────────────────────────────────────────
// Segment 播放
// ─────────────────────────────────────────────
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, System.Action onFinished)
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished)
{
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
if (segment == null)
@@ -313,7 +337,7 @@ namespace OCES.Audio
return handle;
}
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, System.Action onFinished)
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, Action onFinished)
{
yield return new WaitWhile(() => source.isPlaying && !handle.Cancelled);
@@ -370,18 +394,18 @@ namespace OCES.Audio
/// <summary>
/// 一次 Container 播放的句柄,用于外部停止或淡出时访问正在播放的 AudioSource。
/// </summary>
public class ContainerPlayHandle
class ContainerPlayHandle
{
public Coroutine Coroutine;
public bool Cancelled;
public float TargetVolume = 1f;
public List<AudioSource> ActiveSources = new();
public List<ContainerPlayHandle> ChildHandles = new();
internal Coroutine Coroutine;
internal bool Cancelled;
internal float TargetVolume = 1f;
internal List<AudioSource> ActiveSources = new();
internal List<ContainerPlayHandle> ChildHandles = new();
/// <summary>
/// 递归收集所有正在发声的 AudioSource(用于淡出)
/// </summary>
public void CollectActiveSources(List<AudioSource> result)
internal void CollectActiveSources(List<AudioSource> result)
{
result.AddRange(this.ActiveSources);
foreach (ContainerPlayHandle child in this.ChildHandles)