feat: SyncPoint.SameAsCurrentSegment music transition
- 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
This commit is contained in:
@@ -47,8 +47,7 @@ namespace OCES.Audio
|
||||
this.m_fader = new ChannelFader(player, coroutineHost);
|
||||
this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid);
|
||||
|
||||
player.OnContainerEntered += this.m_beatClock.Restart;
|
||||
player.OnBlendError += this.m_beatClock.OnBlendError;
|
||||
player.OnBlendError += this.m_beatClock.OnBlendError;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -103,7 +102,46 @@ namespace OCES.Audio
|
||||
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)
|
||||
{
|
||||
@@ -112,22 +150,39 @@ namespace OCES.Audio
|
||||
}
|
||||
|
||||
// ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ──
|
||||
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break;
|
||||
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));
|
||||
this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState));
|
||||
|
||||
this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||
this.m_fader.FadeInBranch(newContainerId, transition,
|
||||
this.m_fader.FadeInBranch(newContainerId, transition, syncState,
|
||||
onContainerStarted: () =>
|
||||
{
|
||||
// Music 独有:记录新 container 和播放起始时间
|
||||
this.m_currentContainer = this.m_containerConfig.QueryById(newContainerId);
|
||||
this.m_playStartTime = AudioSettings.dspTime;
|
||||
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;
|
||||
@@ -147,23 +202,28 @@ namespace OCES.Audio
|
||||
|
||||
double secondsPerBeat = 60.0 / container.Bpm;
|
||||
|
||||
if (mode == AlignMode.Beat)
|
||||
switch (mode)
|
||||
{
|
||||
double beatsElapsed = elapsed / secondsPerBeat;
|
||||
double nextBeat = System.Math.Ceiling(beatsElapsed);
|
||||
double waitSeconds = (nextBeat - beatsElapsed) * secondsPerBeat;
|
||||
if (waitSeconds > 0.001)
|
||||
yield return new WaitForSeconds((float)waitSeconds);
|
||||
}
|
||||
else if (mode == AlignMode.Bar)
|
||||
{
|
||||
int beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig);
|
||||
double secondsPerBar = secondsPerBeat * beatsPerBar;
|
||||
double barsElapsed = elapsed / secondsPerBar;
|
||||
double nextBar = System.Math.Ceiling(barsElapsed);
|
||||
double waitSeconds = (nextBar - barsElapsed) * secondsPerBar;
|
||||
if (waitSeconds > 0.001)
|
||||
yield return new WaitForSeconds((float)waitSeconds);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user