282 lines
13 KiB
C#
282 lines
13 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
|
|
namespace OCES.Audio
|
|
{
|
|
/// <summary>
|
|
/// 音乐通道播放器。
|
|
/// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。
|
|
/// </summary>
|
|
class MusicChannelPlayer
|
|
{
|
|
readonly MusicContainerConfig m_containerConfig;
|
|
readonly MusicTransitionConfig m_transitionConfig;
|
|
readonly MusicSegmentConfig m_segmentConfig;
|
|
readonly MonoBehaviour m_coroutineHost;
|
|
readonly ChannelFader m_fader;
|
|
readonly BeatClock m_beatClock;
|
|
readonly List<MusicTransition> m_transitionCandidates = new();
|
|
|
|
Coroutine m_currentFadeInCoroutine;
|
|
Coroutine m_currentFadeOutCoroutine;
|
|
|
|
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
|
|
MusicContainer m_currentContainer;
|
|
|
|
// 正在进行的 transition 协程(防止重叠)
|
|
Coroutine m_transitionCoroutine;
|
|
|
|
internal MusicChannelPlayer(
|
|
MusicContainerConfig containerConfig,
|
|
MusicSegmentConfig segmentConfig,
|
|
MusicTransitionConfig transitionConfig,
|
|
LongAudioContainerPlayer player,
|
|
MonoBehaviour coroutineHost,
|
|
Action<uint> onBeat,
|
|
Action<uint> onBar,
|
|
Action<uint> onGrid)
|
|
{
|
|
this.m_containerConfig = containerConfig;
|
|
this.m_transitionConfig = transitionConfig;
|
|
this.m_segmentConfig = segmentConfig;
|
|
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。
|
|
/// </summary>
|
|
internal void SwitchTo(uint newContainerId)
|
|
{
|
|
Debug.Log($"[MusicChannelPlayer] SwitchTo({newContainerId}): CurrentContainerId={this.m_fader.CurrentContainerId}, CurrentHandle={this.m_fader.CurrentHandle}");
|
|
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null)
|
|
return; // 已经在播目标,无需切换
|
|
|
|
MusicTransition transition = ResolveTransition((int)this.m_fader.CurrentContainerId, (int)newContainerId);
|
|
//Debug.Log($"[MusicChannelPlayer] Switch from {this.m_fader.CurrentContainerId} to {newContainerId} with transition {transition.Id}");
|
|
|
|
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)
|
|
{
|
|
Debug.Log($"[MusicChannelPlayer] DoTransition L117: CurrentHandle={this.m_fader.CurrentHandle}, CurrentContainerId={this.m_fader.CurrentContainerId}");
|
|
// 旧 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;
|
|
syncState.SourceStartOffset = GetEffectiveStartOffset(this.m_currentContainer);
|
|
}
|
|
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;
|
|
|
|
double newStartOffset = GetEffectiveStartOffset(container);
|
|
double dspTime;
|
|
|
|
if (syncState is { Mode: SyncPoint.SameAsCurrentSegment })
|
|
{
|
|
double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime;
|
|
int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate);
|
|
double currentAudioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate;
|
|
// 当前的source播到了哪里
|
|
|
|
double currentLogicalTime = Math.Max(0, currentAudioTime - syncState.SourceStartOffset);
|
|
// EntryCue之后播了多少秒。要是小于0说明pre-entry还没播完。
|
|
|
|
double newAudioTime = currentLogicalTime + newStartOffset;
|
|
// 新Audio应该从多少秒开始播放
|
|
|
|
bool isPlayPreEntry = true; //TODO transition 是否播放pre-entry
|
|
syncState.TargetAudioTime = isPlayPreEntry ? currentLogicalTime + newStartOffset : currentLogicalTime;
|
|
dspTime = AudioSettings.dspTime - newAudioTime;
|
|
}
|
|
else
|
|
{
|
|
double startPlayTime;
|
|
if (this.m_currentContainer == null)
|
|
{
|
|
startPlayTime = 0;
|
|
}
|
|
else
|
|
{
|
|
double fadeInTime = transition?.FadeInTime ?? 0f;
|
|
startPlayTime = newStartOffset - fadeInTime;
|
|
startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity);
|
|
}
|
|
syncState.StartPlayTime = startPlayTime;
|
|
dspTime = AudioSettings.dspTime;
|
|
}
|
|
|
|
this.m_currentContainer = container;
|
|
this.m_beatClock.Restart(container, bpm, dspTime, newStartOffset);
|
|
}));
|
|
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 target = this.m_beatClock.GetNextDspTime(mode);
|
|
yield return new WaitUntil(() => AudioSettings.dspTime >= target);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 工具
|
|
// ─────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 查询 Transition 配置,支持精确匹配和 -1 通配符。
|
|
/// </summary>
|
|
MusicTransition ResolveTransition(int sourceContainerId, int destinationContainerId)
|
|
{
|
|
this.m_transitionCandidates.Clear();
|
|
foreach (MusicTransition transition in this.m_transitionConfig.MusicTransitionList())
|
|
{
|
|
bool sourceMatch = transition.SourceContainerID == sourceContainerId || transition.SourceContainerID < 0;
|
|
bool destMatch = transition.DestinationContainerID == destinationContainerId || transition.DestinationContainerID < 0;
|
|
if (sourceMatch && destMatch)
|
|
{
|
|
this.m_transitionCandidates.Add(transition);
|
|
}
|
|
}
|
|
|
|
if (this.m_transitionCandidates.Count == 0)
|
|
return this.m_transitionConfig.QueryById(1);
|
|
|
|
MusicTransition best = this.m_transitionCandidates[0];
|
|
for (int i = 1; i < this.m_transitionCandidates.Count; i++)
|
|
{
|
|
if (this.m_transitionCandidates[i].Id > best.Id)
|
|
best = this.m_transitionCandidates[i];
|
|
}
|
|
return best;
|
|
}
|
|
|
|
double GetEffectiveStartOffset(MusicContainer container)
|
|
{
|
|
if (container.ContainerType == ContainerType.Blend)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
if (container.Segments is not { Count: > 0 })
|
|
return 0.0;
|
|
uint firstId = container.Segments[0];
|
|
if (firstId < 1000000u)
|
|
{
|
|
MusicSegment segment = this.m_segmentConfig.QueryById(firstId);
|
|
return segment?.StartOffset ?? 0.0;
|
|
}
|
|
|
|
MusicContainer child = this.m_containerConfig.QueryById(firstId);
|
|
return child != null ? GetEffectiveStartOffset(child) : 0.0;
|
|
|
|
}
|
|
}
|
|
}
|