6e8e17ec44
- 实现startOffset - 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误。 - 增加数据校验和 BeatClock 联动。StartOffset不正确时停止bar+级 callback。 - BeatClock 现在会在每次重新播放时重启,以解决EndOffset配置错误被舍弃时,拍子对不上的问题。
469 lines
19 KiB
C#
469 lines
19 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using Random = UnityEngine.Random;
|
||
|
||
namespace OCES.Audio
|
||
{
|
||
/// <summary>
|
||
/// 负责递归播放一个 MusicContainer 树。
|
||
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
|
||
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
|
||
/// </summary>
|
||
class LongAudioContainerPlayer
|
||
{
|
||
readonly MusicContainerConfig m_containerConfig;
|
||
readonly MusicSegmentConfig m_segmentConfig;
|
||
readonly AudioSourcePool m_pool;
|
||
readonly MonoBehaviour m_coroutineHost;
|
||
UnityEngine.Audio.AudioMixerGroup m_mixerGroup;
|
||
|
||
/// <summary>
|
||
/// 开始播放Container时触发 音乐回调系统
|
||
/// container本身,继承来的bpm,进入时刻的dspTime
|
||
/// </summary>
|
||
internal event Action<MusicContainer, float, double> OnContainerEntered, OnContainerLooped;
|
||
internal event Action<MusicContainer> OnBlendError;
|
||
|
||
// Sequence Step 模式的全局游标,key = containerId
|
||
readonly Dictionary<uint, int> m_sequenceStepIndex = new();
|
||
|
||
// Random 模式的不放回历史,key = containerId
|
||
readonly Dictionary<uint, HashSet<uint>> m_randomHistory = new();
|
||
|
||
public LongAudioContainerPlayer(
|
||
MusicContainerConfig containerConfig,
|
||
MusicSegmentConfig segmentConfig,
|
||
AudioSourcePool pool,
|
||
MonoBehaviour coroutineHost)
|
||
{
|
||
this.m_containerConfig = containerConfig;
|
||
this.m_segmentConfig = segmentConfig;
|
||
this.m_pool = pool;
|
||
this.m_coroutineHost = coroutineHost;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 公开入口
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 开始播放指定 Container,播放完毕后调用 onFinished。
|
||
/// 返回可用于外部停止的句柄。
|
||
/// </summary>
|
||
internal ContainerPlayHandle Play(uint containerId, Action onFinished = null, float inheritedBpm = 0f)
|
||
{
|
||
MusicContainer container = this.m_containerConfig.QueryById(containerId);
|
||
if (container == null)
|
||
{
|
||
Debug.LogError($"[LongAudioContainerPlayer] 找不到 ContainerId: {containerId}");
|
||
onFinished?.Invoke();
|
||
return null;
|
||
}
|
||
|
||
ContainerPlayHandle handle = new();
|
||
handle.TargetVolume = 1f;
|
||
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
|
||
PlayContainerCoroutine(container, handle, inheritedBpm, onFinished));
|
||
return handle;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止一个播放句柄(立即停止,不淡出)
|
||
/// </summary>
|
||
public void Stop(ContainerPlayHandle handle)
|
||
{
|
||
if (handle == null) return;
|
||
handle.Cancelled = true;
|
||
|
||
foreach (ContainerPlayHandle child in handle.ChildHandles)
|
||
{
|
||
Stop(child);
|
||
}
|
||
handle.ChildHandles.Clear();
|
||
|
||
|
||
if (handle.Coroutine != null)
|
||
this.m_coroutineHost.StopCoroutine(handle.Coroutine);
|
||
foreach (AudioSource src in handle.ActiveSources)
|
||
{
|
||
ReturnSource(src);
|
||
}
|
||
handle.ActiveSources.Clear();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 核心协程
|
||
// ─────────────────────────────────────────────
|
||
|
||
IEnumerator PlayContainerCoroutine(
|
||
MusicContainer container,
|
||
ContainerPlayHandle handle,
|
||
float inheritedBpm,
|
||
Action onFinished)
|
||
{
|
||
float effectiveBpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
|
||
//Debug.Log($"[LongAudioContainerPlayer] OnContainerEntered firing, container={container.Id}, subscribers={OnContainerEntered != null}");
|
||
OnContainerEntered?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
|
||
|
||
int loopsCompleted = 0;
|
||
|
||
while (true)
|
||
{
|
||
bool isLoop = loopsCompleted > 0;
|
||
yield return this.m_coroutineHost.StartCoroutine(
|
||
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm, isLoop));
|
||
|
||
if (handle.Cancelled) yield break;
|
||
|
||
loopsCompleted++;
|
||
OnContainerLooped?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
|
||
|
||
// -1 = 无限循环,一直重复
|
||
if (container.LoopCount == -1)
|
||
continue;
|
||
|
||
// 0 = 不循环,播一次就结束
|
||
if (container.LoopCount == 0)
|
||
break;
|
||
|
||
// >= 1,播满次数后结束
|
||
if (loopsCompleted >= container.LoopCount)
|
||
break;
|
||
}
|
||
|
||
if (!handle.Cancelled)
|
||
onFinished?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放一个 Container 一轮(不含循环逻辑)
|
||
/// </summary>
|
||
IEnumerator PlayContainerOnce(
|
||
MusicContainer container,
|
||
float volumeScale,
|
||
ContainerPlayHandle handle,
|
||
float inheritedBpm,
|
||
bool isLoop = false)
|
||
{
|
||
if (container.Segments == null || container.Segments.Count == 0)
|
||
yield break;
|
||
|
||
switch (container.ContainerType)
|
||
{
|
||
case ContainerType.Blend:
|
||
yield return PlayBlend(container, volumeScale, handle, inheritedBpm);
|
||
break;
|
||
|
||
case ContainerType.Sequence:
|
||
yield return PlaySequence(container, volumeScale, handle, inheritedBpm, isLoop);
|
||
break;
|
||
|
||
case ContainerType.Random:
|
||
yield return PlayRandom(container, volumeScale, handle, inheritedBpm, isLoop);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Blend(同时播放)
|
||
// ─────────────────────────────────────────────
|
||
|
||
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);
|
||
}
|
||
|
||
// 同时启动所有子元素,等待全部结束
|
||
List<ContainerPlayHandle> childHandles = new();
|
||
bool allDone = false;
|
||
int remaining = container.Segments.Count;
|
||
|
||
foreach (uint segId in container.Segments)
|
||
{
|
||
if (handle.Cancelled) yield break;
|
||
|
||
ContainerPlayHandle childHandle = PlayChild(segId, volumeScale, () =>
|
||
{
|
||
remaining--;
|
||
if (remaining <= 0) allDone = true;
|
||
}, inheritedBpm);
|
||
if (childHandle != null)
|
||
{
|
||
childHandles.Add(childHandle);
|
||
handle.ChildHandles.AddRange(childHandles);
|
||
}
|
||
}
|
||
|
||
yield return new WaitUntil(() => allDone || handle.Cancelled);
|
||
|
||
if (handle.Cancelled)
|
||
{
|
||
foreach (var ch in childHandles)
|
||
Stop(ch);
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Sequence
|
||
// ─────────────────────────────────────────────
|
||
|
||
IEnumerator PlaySequence(
|
||
MusicContainer container,
|
||
float volumeScale,
|
||
ContainerPlayHandle handle,
|
||
float inheritedBpm,
|
||
bool isLoop = false)
|
||
{
|
||
bool isStep = container.ContainerPlayMode;
|
||
|
||
if (isStep)
|
||
{
|
||
// Step: 每次只播一个,游标全局推进
|
||
int index = GetNextSequenceIndex(container);
|
||
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm, isLoop);
|
||
|
||
}
|
||
else
|
||
{
|
||
// Continuous: 顺序播完所有子元素,算一轮
|
||
for (int i = 0; i < container.Segments.Count; i++)
|
||
{
|
||
if (handle.Cancelled) yield break;
|
||
|
||
// StrategyParam 作为两段之间的间隔时间(秒)
|
||
if (i > 0 && container.StrategyParam > 0)
|
||
yield return new WaitForSeconds(container.StrategyParam);
|
||
|
||
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm, isLoop);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Random
|
||
// ─────────────────────────────────────────────
|
||
|
||
IEnumerator PlayRandom(
|
||
MusicContainer container,
|
||
float volumeScale,
|
||
ContainerPlayHandle handle,
|
||
float inheritedBpm,
|
||
bool isLoop = false)
|
||
{
|
||
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
|
||
|
||
if (isStep)
|
||
{
|
||
// Step Random: 随机选一个播放,算一次 loopCount
|
||
uint chosen = PickRandomChild(container);
|
||
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
|
||
}
|
||
else
|
||
{
|
||
// Continuous Random: 随机不放回地播完所有子元素,算一轮
|
||
var remaining = new List<uint>(container.Segments);
|
||
|
||
while (remaining.Count > 0)
|
||
{
|
||
if (handle.Cancelled) yield break;
|
||
|
||
int idx = Random.Range(0, remaining.Count);
|
||
uint chosen = remaining[idx];
|
||
remaining.RemoveAt(idx);
|
||
|
||
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
|
||
yield return new WaitForSeconds(container.StrategyParam);
|
||
|
||
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 子元素分发(Segment 或嵌套 Container)
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 播放一个子元素(segment 或 container),等待其完成后返回。
|
||
/// </summary>
|
||
IEnumerator PlayChildAndWait(uint id,
|
||
float volumeScale,
|
||
ContainerPlayHandle parentHandle,
|
||
float inheritedBpm,
|
||
bool isLoop = false)
|
||
{
|
||
bool done = false;
|
||
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm, isLoop);
|
||
if (child != null)
|
||
parentHandle.ChildHandles.Add(child);
|
||
|
||
yield return new WaitUntil(() => done || parentHandle.Cancelled);
|
||
|
||
if (!parentHandle.Cancelled || child == null)
|
||
yield break;
|
||
Stop(child);
|
||
parentHandle.ChildHandles.Remove(child);
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动一个子元素的播放,不等待,返回句柄。
|
||
/// </summary>
|
||
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm, bool isLoop = false)
|
||
{
|
||
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
|
||
return id < 1000000u ? PlaySegment(id, volumeScale, onDone, isLoop) : Play(id, onDone, inheritedBpm);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Segment 播放
|
||
// ─────────────────────────────────────────────
|
||
|
||
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished, bool isLoop = false)
|
||
{
|
||
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
|
||
if (segment == null)
|
||
{
|
||
Debug.LogError($"[LongAudioContainerPlayer] 找不到 SegmentId: {segmentId}");
|
||
onFinished?.Invoke();
|
||
return null;
|
||
}
|
||
|
||
AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
|
||
if (!clip)
|
||
{
|
||
Debug.LogError($"[LongAudioContainerPlayer] 音频文件未找到: {segment.Name}");
|
||
onFinished?.Invoke();
|
||
return null;
|
||
}
|
||
|
||
AudioSource source = this.m_pool.AcquireAudioSource();
|
||
source.clip = clip;
|
||
source.loop = false;
|
||
source.volume = volumeScale;
|
||
source.Play();
|
||
Debug.Log($"[LongAudioContainerPlayer] Playing {segment.Name} at {AudioSettings.dspTime}");
|
||
|
||
if (isLoop && segment.StartOffset > 0)
|
||
{
|
||
source.time = (float)segment.StartOffset;
|
||
}
|
||
|
||
ContainerPlayHandle handle = new();
|
||
handle.ActiveSources.Add(source);
|
||
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
|
||
WaitSegmentFinish(source, handle, onFinished, segment.EndOffset));
|
||
return handle;
|
||
}
|
||
|
||
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, Action onFinished, double endOffset)
|
||
{
|
||
double effectiveTime = endOffset > 0f ? source.clip.length - endOffset : source.clip.length;
|
||
|
||
// 1. 等待"逻辑结束"(EndOffset 或自然结束)
|
||
yield return new WaitWhile(() =>
|
||
source.isPlaying &&
|
||
!handle.Cancelled &&
|
||
source.time < effectiveTime);
|
||
|
||
// 2. 立即通知 container 推进
|
||
if (!handle.Cancelled) onFinished?.Invoke();
|
||
|
||
// 3. 如果 source 还在物理播放(EndOffset 提前退出的情况),等它自然结束并清理资源
|
||
yield return new WaitWhile(() => source.isPlaying && !handle.Cancelled);
|
||
source.Stop();
|
||
ReturnSource(source);
|
||
handle.ActiveSources.Remove(source);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 工具
|
||
// ─────────────────────────────────────────────
|
||
|
||
void ReturnSource(AudioSource source)
|
||
{
|
||
source.Stop();
|
||
this.m_pool.ReturnToPool(source.gameObject);
|
||
}
|
||
|
||
int GetNextSequenceIndex(MusicContainer container)
|
||
{
|
||
int current = this.m_sequenceStepIndex.GetValueOrDefault(container.Id, 0);
|
||
this.m_sequenceStepIndex[container.Id] = (current + 1) % container.Segments.Count;
|
||
return current;
|
||
}
|
||
|
||
uint PickRandomChild(MusicContainer container)
|
||
{
|
||
if (!this.m_randomHistory.TryGetValue(container.Id, out HashSet<uint> history))
|
||
{
|
||
history = new HashSet<uint>();
|
||
this.m_randomHistory[container.Id] = history;
|
||
}
|
||
|
||
List<uint> available = container.Segments.Where(id => !history.Contains(id)).ToList();
|
||
if (available.Count == 0)
|
||
{
|
||
history.Clear();
|
||
available = new List<uint>(container.Segments);
|
||
}
|
||
|
||
uint chosen = available[Random.Range(0, available.Count)];
|
||
history.Add(chosen);
|
||
return chosen;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 播放句柄
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 一次 Container 播放的句柄,用于外部停止或淡出时访问正在播放的 AudioSource。
|
||
/// </summary>
|
||
class ContainerPlayHandle
|
||
{
|
||
internal Coroutine Coroutine;
|
||
internal bool Cancelled;
|
||
internal float TargetVolume = 1f;
|
||
internal List<AudioSource> ActiveSources = new();
|
||
internal List<ContainerPlayHandle> ChildHandles = new();
|
||
|
||
/// <summary>
|
||
/// 递归收集所有正在发声的 AudioSource(用于淡出)
|
||
/// </summary>
|
||
internal void CollectActiveSources(List<AudioSource> result)
|
||
{
|
||
result.AddRange(this.ActiveSources);
|
||
foreach (ContainerPlayHandle child in this.ChildHandles)
|
||
child.CollectActiveSources(result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取第一个正在播放的 Leaf Segment 的 AudioSource。
|
||
/// 递归遍历子句柄,优先取当前层级的 ActiveSources,找不到再深入子层级。
|
||
/// </summary>
|
||
internal AudioSource GetFirstLeafSource()
|
||
{
|
||
if (this.ActiveSources.Count > 0) return this.ActiveSources[0];
|
||
foreach (ContainerPlayHandle child in this.ChildHandles)
|
||
{
|
||
AudioSource src = child.GetFirstLeafSource();
|
||
if (src) return src;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
}
|