0fdd76022d
- Add SameAsCurrentSegment mode to align new container's timeSamples with the old container's playback position, accounting for FadeInOffset - Fix BeatClock callback burst when Restart is called with a past dspTime - Add GetFirstLeafSource() for resolving playback position across nested containers - Manual BeatClock.Restart replaces OnContainerEntered subscription for accurate timing with SyncPoint
261 lines
12 KiB
C#
261 lines
12 KiB
C#
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
|
||
namespace OCES.Audio
|
||
{
|
||
/// <summary>
|
||
/// 音乐通道播放器。
|
||
/// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。
|
||
/// </summary>
|
||
class MusicChannelPlayer
|
||
{
|
||
readonly MusicContainerConfig m_containerConfig;
|
||
readonly MusicTransitionConfig m_transitionConfig;
|
||
readonly MonoBehaviour m_coroutineHost;
|
||
readonly ChannelFader m_fader;
|
||
readonly BeatClock m_beatClock;
|
||
|
||
Coroutine m_currentFadeInCoroutine;
|
||
Coroutine m_currentFadeOutCoroutine;
|
||
|
||
// 当前正在播放的句柄
|
||
ContainerPlayHandle m_currentHandle;
|
||
uint m_currentContainerId;
|
||
|
||
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
|
||
MusicContainer m_currentContainer;
|
||
|
||
// 当前播放开始的时间(用于计算当前播到哪个拍子)
|
||
double m_playStartTime;
|
||
|
||
// 正在进行的 transition 协程(防止重叠)
|
||
Coroutine m_transitionCoroutine;
|
||
|
||
internal MusicChannelPlayer(
|
||
MusicContainerConfig containerConfig,
|
||
MusicTransitionConfig transitionConfig,
|
||
MusicContainerPlayer player,
|
||
MonoBehaviour coroutineHost,
|
||
Action<uint> onBeat,
|
||
Action<uint> onBar,
|
||
Action<uint> onGrid)
|
||
{
|
||
this.m_containerConfig = containerConfig;
|
||
this.m_transitionConfig = transitionConfig;
|
||
this.m_coroutineHost = coroutineHost;
|
||
this.m_fader = new ChannelFader(player, coroutineHost);
|
||
this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid);
|
||
|
||
player.OnBlendError += this.m_beatClock.OnBlendError;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 公开接口
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 切换到新的 Container。
|
||
/// fromPathId / toPathId 用于查询 Transition 配置。
|
||
/// </summary>
|
||
internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
|
||
{
|
||
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
||
return; // 已经在播目标,无需切换
|
||
|
||
MusicTransition transition = ResolveTransition(fromPathId, toPathId);
|
||
|
||
if (this.m_transitionCoroutine != null)
|
||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||
|
||
this.m_transitionCoroutine = this.m_coroutineHost.StartCoroutine(
|
||
DoTransition(newContainerId, transition));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即停止当前播放(无淡出)
|
||
/// </summary>
|
||
internal void Stop()
|
||
{
|
||
this.m_beatClock.StopAll();
|
||
|
||
if (this.m_transitionCoroutine != null)
|
||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||
|
||
this.m_fader.StopCurrent();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Transition 流程
|
||
// ─────────────────────────────────────────────
|
||
|
||
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;
|
||
}
|
||
|
||
// ── 构建 SyncPoint 状态 ──
|
||
SyncPointState syncState = new()
|
||
{
|
||
Mode = transition?.SyncPoint ?? SyncPoint.Start,
|
||
Ready = false,
|
||
};
|
||
|
||
//Debug.Log($"[MusicChannelPlayer] DoTransition: transition?.SyncPoint={transition?.SyncPoint}, Mode={syncState.Mode}");
|
||
|
||
if (syncState.Mode == SyncPoint.SameAsCurrentSegment)
|
||
{
|
||
// 旧 Container 不存在(如游戏首次启动),静默降级为 Start
|
||
if (this.m_fader.CurrentHandle == null)
|
||
{
|
||
syncState.Mode = SyncPoint.Start;
|
||
}
|
||
// Blend 类型不支持 SameAsCurrentSegment,降级为 Start
|
||
else if (this.m_currentContainer is { ContainerType: ContainerType.Blend })
|
||
{
|
||
Debug.LogWarning($"[MusicChannelPlayer] SyncPoint.SameAsCurrentSegment 不支持 Blend 类型 Container({this.m_currentContainer.Id}),降级为 Start");
|
||
syncState.Mode = SyncPoint.Start;
|
||
}
|
||
else
|
||
{
|
||
AudioSource oldSource = this.m_fader.CurrentHandle.GetFirstLeafSource();
|
||
if (oldSource && oldSource.clip)
|
||
{
|
||
syncState.BaseTimeSamples = oldSource.timeSamples;
|
||
syncState.BaseDspTime = AudioSettings.dspTime;
|
||
syncState.SampleRate = oldSource.clip.frequency;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[MusicChannelPlayer] SameAsCurrentSegment: 旧 Source 不存在,降级为 Start");
|
||
syncState.Mode = SyncPoint.Start;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)──
|
||
if (transition != null && this.m_currentContainer != null)
|
||
{
|
||
yield return this.m_coroutineHost.StartCoroutine(
|
||
WaitForAlignment(transition.AlignMode, this.m_currentContainer));
|
||
}
|
||
|
||
// ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ──
|
||
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
|
||
// 如果等待期间被切回来了,就不淡变了
|
||
|
||
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, syncState));
|
||
|
||
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
|
||
this.m_fader.FadeInBranch(newContainerId, transition, syncState,
|
||
onContainerStarted: () =>
|
||
{
|
||
MusicContainer container = this.m_containerConfig.QueryById(newContainerId);
|
||
float bpm = container.Bpm > 0f ? container.Bpm : 120f;
|
||
|
||
// SyncPoint: BeatClock 用调整后的 dspTime,对齐到音频实际播放位置
|
||
double dspTime;
|
||
if (syncState is { Mode: SyncPoint.SameAsCurrentSegment })
|
||
{
|
||
double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime;
|
||
int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate);
|
||
double audioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate;
|
||
dspTime = AudioSettings.dspTime - audioTime;
|
||
}
|
||
else
|
||
{
|
||
dspTime = AudioSettings.dspTime;
|
||
}
|
||
|
||
this.m_currentContainer = container;
|
||
this.m_playStartTime = dspTime;
|
||
this.m_beatClock.Restart(container, bpm, dspTime);
|
||
}));
|
||
yield return this.m_currentFadeInCoroutine;
|
||
this.m_currentFadeInCoroutine = null;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 节拍对齐等待
|
||
// ─────────────────────────────────────────────
|
||
|
||
IEnumerator WaitForAlignment(AlignMode mode, MusicContainer container)
|
||
{
|
||
if (mode == AlignMode.Immediate || container.Bpm <= 0f)
|
||
yield break;
|
||
|
||
double now = AudioSettings.dspTime;
|
||
double elapsed = now - this.m_playStartTime;
|
||
|
||
double secondsPerBeat = 60.0 / container.Bpm;
|
||
|
||
switch (mode)
|
||
{
|
||
case AlignMode.Beat:
|
||
{
|
||
double beatsElapsed = elapsed / secondsPerBeat;
|
||
double nextBeat = Math.Ceiling(beatsElapsed);
|
||
double waitSeconds = (nextBeat - beatsElapsed) * secondsPerBeat;
|
||
if (waitSeconds > 0.001)
|
||
yield return new WaitForSeconds((float)waitSeconds);
|
||
break;
|
||
}
|
||
case AlignMode.Bar:
|
||
{
|
||
int beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig);
|
||
double secondsPerBar = secondsPerBeat * beatsPerBar;
|
||
double barsElapsed = elapsed / secondsPerBar;
|
||
double nextBar = Math.Ceiling(barsElapsed);
|
||
double waitSeconds = (nextBar - barsElapsed) * secondsPerBar;
|
||
if (waitSeconds > 0.001)
|
||
yield return new WaitForSeconds((float)waitSeconds);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 工具
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 查询 Transition 配置,支持精确匹配和 999 通配符。
|
||
/// ID 规则:FromPathId × 1000 + ToPathId,999 表示任意。
|
||
/// </summary>
|
||
MusicTransition ResolveTransition(uint fromPathId, uint toPathId)
|
||
{
|
||
// 优先精确匹配
|
||
uint exactId = fromPathId * 1000 + toPathId;
|
||
MusicTransition exact = this.m_transitionConfig.QueryById(exactId);
|
||
if (exact != null) return exact;
|
||
|
||
// From 为任意
|
||
uint fromWildcard = 999u * 1000 + toPathId;
|
||
MusicTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
|
||
if (fromWild != null) return fromWild;
|
||
|
||
// To 为任意
|
||
uint toWildcard = fromPathId * 1000 + 999u;
|
||
MusicTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
|
||
if (toWild != null) return toWild;
|
||
|
||
// 全通配
|
||
const uint allWild = 999u * 1000 + 999u;
|
||
return this.m_transitionConfig.QueryById(allWild);
|
||
}
|
||
}
|
||
}
|