WIP: StartOffset

This commit is contained in:
2026-05-07 12:09:16 +08:00
parent 8ea862b546
commit ab6e9e74e0
51 changed files with 810 additions and 64 deletions
@@ -11,6 +11,7 @@ namespace OCES.Audio
double m_startDspTime, m_secondsPerBeat; // 本次计数周期的起点
Coroutine m_beatCoroutine, m_barCoroutine, m_gridCoroutine;
Coroutine m_delayCoroutine;
readonly Action<uint> m_onBeat, m_onBar, m_onGrid;
readonly MonoBehaviour m_host;
@@ -26,13 +27,13 @@ namespace OCES.Audio
this.m_onGrid = onGrid;
}
internal void Restart(MusicContainer container, float inheritedBpm, double dspTime)
internal void Restart(MusicContainer container, float inheritedBpm, double dspTime, double startOffset = 0d)
{
//Debug.Log($"[BeatClock] Restarting {container.Id}, inheritedBpm = {inheritedBpm}, dspTime = {dspTime}");
StopAll();
this.m_blendError = this.m_stopped = false;
this.m_containerId = container.Id;
this.m_startDspTime = dspTime;
this.m_startDspTime = dspTime + startOffset;
// BPM:优先用自己的,为 0 则用传入的继承值
float bpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
@@ -57,6 +58,32 @@ namespace OCES.Audio
this.m_barsPerGrid = container.Grid;
}
double delay = this.m_startDspTime - AudioSettings.dspTime;
if (delay > 0)
{
this.m_delayCoroutine = this.m_host.StartCoroutine(DelayedStart(delay, hasTimeSig, hasGrid));
}
else
{
StartCoroutines(hasTimeSig, hasGrid);
}
}
IEnumerator DelayedStart(double delay, bool hasTimeSig, bool hasGrid)
{
yield return new WaitUntil(() => AudioSettings.dspTime >= this.m_startDspTime - 0.02);
while (AudioSettings.dspTime < this.m_startDspTime)
yield return null;
if (this.m_stopped || this.m_blendError) yield break;
this.m_delayCoroutine = null;
StartCoroutines(hasTimeSig, hasGrid);
}
void StartCoroutines(bool hasTimeSig, bool hasGrid)
{
// 分层启动协程
// Beat:只要 BPM 有效就启动
this.m_beatCoroutine = this.m_host.StartCoroutine(BeatCoroutine());
@@ -82,10 +109,11 @@ namespace OCES.Audio
internal void StopAll()
{
this.m_stopped = true;
if (this.m_beatCoroutine != null) this.m_host.StopCoroutine(this.m_beatCoroutine);
if (this.m_barCoroutine != null) this.m_host.StopCoroutine(this.m_barCoroutine);
if (this.m_gridCoroutine != null) this.m_host.StopCoroutine(this.m_gridCoroutine);
this.m_beatCoroutine = this.m_barCoroutine = this.m_gridCoroutine = null;
if (this.m_delayCoroutine != null) this.m_host.StopCoroutine(this.m_delayCoroutine);
if (this.m_beatCoroutine != null) this.m_host.StopCoroutine(this.m_beatCoroutine);
if (this.m_barCoroutine != null) this.m_host.StopCoroutine(this.m_barCoroutine);
if (this.m_gridCoroutine != null) this.m_host.StopCoroutine(this.m_gridCoroutine);
this.m_delayCoroutine = this.m_beatCoroutine = this.m_barCoroutine = this.m_gridCoroutine = null;
}
internal double GetNextDspTime(AlignMode mode)
@@ -98,6 +126,11 @@ namespace OCES.Audio
}
double elapsed = now - this.m_startDspTime;
if (elapsed < 0)
{
return this.m_startDspTime;
}
double period = mode switch
{
AlignMode.Beat => this.m_secondsPerBeat,
@@ -16,6 +16,9 @@ namespace OCES.Audio
public double BaseDspTime; // 读取Source Segment 的 dspTime
public int SampleRate; // Source Segment 的采样率
public bool Ready;
public double SourceStartOffset;
public double TargetAudioTime;
public double StartPlayTime;
}
/// <summary>
@@ -41,10 +44,12 @@ namespace OCES.Audio
CurrentContainerId = containerId;
CurrentVolume = startVolume;
CurrentHandle = this.m_player.Play(containerId);
Debug.Log($"[ChannelFader] StartNew: containerId={containerId}, CurrentHandle={CurrentHandle}");
}
public void StopCurrent()
{
Debug.Log($"[ChannelFader] StopCurrent called! CurrentHandle={CurrentHandle}, stack=\n{Environment.StackTrace}");
StopHandle(CurrentHandle);
CurrentHandle = null;
CurrentContainerId = 0;
@@ -84,7 +89,8 @@ namespace OCES.Audio
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
/// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。
/// </summary>
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, SyncPointState syncState = null, Action onContainerStarted = null)
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, SyncPointState syncState = null,
Action onContainerStarted = null)
{
if (transition?.FadeInOffset > 0f)
{
@@ -94,7 +100,7 @@ namespace OCES.Audio
if (newContainerId == 0)
{
CurrentHandle = null;
CurrentHandle = null;
CurrentContainerId = 0;
if (syncState != null) syncState.Ready = true;
yield break;
@@ -102,43 +108,63 @@ namespace OCES.Audio
float startVolume = transition?.FadeInTime > 0f ? 0f : 1f;
StartNew(newContainerId, startVolume);
onContainerStarted?.Invoke();
// SyncPoint: 获取新 Segment 的 AudioSource 并设置 timeSamples
if (syncState is { Mode: SyncPoint.SameAsCurrentSegment })
switch (syncState)
{
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (newSource && newSource.clip)
// SyncPoint: 获取新 Segment 的 AudioSource 并设置 timeSamples
case { Mode: SyncPoint.SameAsCurrentSegment }:
{
// 计算从读取Source Segment 到现在经过的 samples
double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime;
int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate);
int targetTimeSamples = syncState.BaseTimeSamples + elapsedSamples;
if (targetTimeSamples < newSource.clip.samples)
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (newSource && newSource.clip)
{
newSource.timeSamples = targetTimeSamples;
double targetAudioTime = syncState.TargetAudioTime;
if (targetAudioTime > 0)
{
int targetTimeSamples = (int)(targetAudioTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
else
{
Debug.LogError(
$"[ChannelFader] SyncPoint: 目标 samples({targetTimeSamples}) 超出范围 [0, {newSource.clip.samples}),降级为 Start");
}
}
}
else
{
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 新音频 samples({newSource.clip.samples}) <= 目标 samples({targetTimeSamples}),降级为 Start");
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 未能获取新 Segment 的 AudioSource,降级为 Start");
}
syncState.Ready = true;
break;
}
else
case { Mode: SyncPoint.Start, StartPlayTime: > 0 }:
{
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 未能获取新 Segment 的 AudioSource,降级为 Start");
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (!newSource || !newSource.clip)
yield break;
//在赋值的时候减过fade in time了
int targetTimeSamples = (int)(syncState.StartPlayTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
break;
}
syncState.Ready = true;
}
onContainerStarted?.Invoke();
if (transition?.FadeInTime > 0f)
{
yield return this.m_coroutineHost.StartCoroutine(
FadeIn(CurrentHandle, transition.FadeInTime));
}
}
IEnumerator FadeOut(ContainerPlayHandle handle, float fromVolume, float duration)
{
//Debug.Log($"Fading out in {duration} seconds.");
@@ -112,8 +112,9 @@ namespace OCES.Audio
while (true)
{
bool isLoop = loopsCompleted > 0;
yield return this.m_coroutineHost.StartCoroutine(
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm));
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm, isLoop));
if (handle.Cancelled) yield break;
@@ -139,7 +140,12 @@ namespace OCES.Audio
/// <summary>
/// 播放一个 Container 一轮(不含循环逻辑)
/// </summary>
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlayContainerOnce(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
if (container.Segments == null || container.Segments.Count == 0)
yield break;
@@ -151,11 +157,11 @@ namespace OCES.Audio
break;
case ContainerType.Sequence:
yield return PlaySequence(container, volumeScale, handle, inheritedBpm);
yield return PlaySequence(container, volumeScale, handle, inheritedBpm, isLoop);
break;
case ContainerType.Random:
yield return PlayRandom(container, volumeScale, handle, inheritedBpm);
yield return PlayRandom(container, volumeScale, handle, inheritedBpm, isLoop);
break;
}
}
@@ -211,7 +217,12 @@ namespace OCES.Audio
// Sequence
// ─────────────────────────────────────────────
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlaySequence(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
bool isStep = container.ContainerPlayMode;
@@ -219,7 +230,7 @@ namespace OCES.Audio
{
// Step: 每次只播一个,游标全局推进
int index = GetNextSequenceIndex(container);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm, isLoop);
}
else
@@ -233,7 +244,7 @@ namespace OCES.Audio
if (i > 0 && container.StrategyParam > 0)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm, isLoop);
}
}
}
@@ -242,7 +253,12 @@ namespace OCES.Audio
// Random
// ─────────────────────────────────────────────
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlayRandom(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
@@ -250,7 +266,7 @@ namespace OCES.Audio
{
// Step Random: 随机选一个播放,算一次 loopCount
uint chosen = PickRandomChild(container);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
}
else
{
@@ -268,7 +284,7 @@ namespace OCES.Audio
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
}
}
}
@@ -280,10 +296,14 @@ namespace OCES.Audio
/// <summary>
/// 播放一个子元素(segment 或 container),等待其完成后返回。
/// </summary>
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle, float inheritedBpm)
IEnumerator PlayChildAndWait(uint id,
float volumeScale,
ContainerPlayHandle parentHandle,
float inheritedBpm,
bool isLoop = false)
{
bool done = false;
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm);
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm, isLoop);
if (child != null)
parentHandle.ChildHandles.Add(child);
@@ -299,17 +319,17 @@ namespace OCES.Audio
/// <summary>
/// 启动一个子元素的播放,不等待,返回句柄。
/// </summary>
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm)
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm, bool isLoop = false)
{
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone, inheritedBpm: inheritedBpm);
return id < 1000000u ? PlaySegment(id, volumeScale, onDone, isLoop) : Play(id, onDone, inheritedBpm);
}
// ─────────────────────────────────────────────
// Segment 播放
// ─────────────────────────────────────────────
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished)
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished, bool isLoop = false)
{
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
if (segment == null)
@@ -333,7 +353,12 @@ namespace OCES.Audio
source.volume = volumeScale;
source.Play();
var handle = new ContainerPlayHandle();
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));
@@ -342,7 +367,7 @@ namespace OCES.Audio
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, Action onFinished, double endOffset)
{
double effectiveTime = endOffset > 0f ? source.clip.length - endOffset : 0f;
double effectiveTime = endOffset > 0f ? source.clip.length - endOffset : source.clip.length;
// 1. 等待"逻辑结束"EndOffset 或自然结束)
yield return new WaitWhile(() =>
@@ -14,6 +14,7 @@ namespace OCES.Audio
{
readonly MusicContainerConfig m_containerConfig;
readonly MusicTransitionConfig m_transitionConfig;
readonly MusicSegmentConfig m_segmentConfig;
readonly MonoBehaviour m_coroutineHost;
readonly ChannelFader m_fader;
readonly BeatClock m_beatClock;
@@ -30,6 +31,7 @@ namespace OCES.Audio
internal MusicChannelPlayer(
MusicContainerConfig containerConfig,
MusicSegmentConfig segmentConfig,
MusicTransitionConfig transitionConfig,
LongAudioContainerPlayer player,
MonoBehaviour coroutineHost,
@@ -39,6 +41,7 @@ namespace OCES.Audio
{
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);
@@ -55,6 +58,7 @@ namespace OCES.Audio
/// </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; // 已经在播目标,无需切换
@@ -110,6 +114,7 @@ namespace OCES.Audio
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)
{
@@ -129,6 +134,7 @@ namespace OCES.Audio
syncState.BaseTimeSamples = oldSource.timeSamples;
syncState.BaseDspTime = AudioSettings.dspTime;
syncState.SampleRate = oldSource.clip.frequency;
syncState.SourceStartOffset = GetEffectiveStartOffset(this.m_currentContainer);
}
else
{
@@ -161,23 +167,46 @@ namespace OCES.Audio
{
MusicContainer container = this.m_containerConfig.QueryById(newContainerId);
float bpm = container.Bpm;
// SyncPoint: BeatClock 用调整后的 dspTime,对齐到音频实际播放位置
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 audioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate;
dspTime = AudioSettings.dspTime - audioTime;
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);
this.m_beatClock.Restart(container, bpm, dspTime, newStartOffset);
}));
yield return this.m_currentFadeInCoroutine;
this.m_currentFadeInCoroutine = null;
@@ -227,5 +256,26 @@ namespace OCES.Audio
}
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;
}
}
}
@@ -0,0 +1,10 @@
namespace OCES.Audio
{
public partial class MusicSegmentConfig
{
// TODO: 运行前边界验证
// - MusicSegment.StartOffset <= AudioClip.length
// - MusicSegment.EndOffset <= AudioClip.length
// - MusicSegment.StartOffset + MusicSegment.EndOffset <= AudioClip.length
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5c388a0ba2794e1ca09ccdde5bd5f99a
timeCreated: 1778049644
@@ -40,7 +40,7 @@ namespace OCES.Audio
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
this.m_musicChannel = new MusicChannelPlayer(
containers, musicTransitions, longAudioContainerPlayer, this,
containers, segments, musicTransitions, longAudioContainerPlayer, this,
id => OnBeat?.Invoke(id),
id => OnBar?.Invoke(id),
id => OnGrid?.Invoke(id));