Files
AudioSystem/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/LongAudioContainerPlayer.cs
T

474 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}");
AudioClip clip = null;
AudioSystem.Instance.ResourceLoader.LoadAsync<AudioClip>($"{AudioExtendSettings.Instance.audioResourcePath}/{segment.Name}", loadedClip =>
{
clip = loadedClip;
});
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;
}
}
}