Files
AudioSystem/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs
T
Oliver 6e8e17ec44 feat: StartOffset
- 实现startOffset
- 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误。
- 增加数据校验和 BeatClock 联动。StartOffset不正确时停止bar+级 callback。
- BeatClock 现在会在每次重新播放时重启,以解决EndOffset配置错误被舍弃时,拍子对不上的问题。
2026-05-12 15:47:04 +08:00

205 lines
7.4 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 UnityEngine;
namespace OCES.Audio
{
public class BeatClock
{
int m_beatsPerBar; // 从 TimeSig 解析
int m_barsPerGrid; // 从 Grid 字段读取
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;
uint m_containerId;
bool m_stopped, m_blendError; // 检测到 Blend 多 BPM 情况时置为 true
internal BeatClock(MonoBehaviour host, Action<uint> onBeat, Action<uint> onBar, Action<uint> onGrid)
{
this.m_host = host;
this.m_onBeat = onBeat;
this.m_onBar = onBar;
this.m_onGrid = onGrid;
}
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 + startOffset;
// BPM:优先用自己的,为 0 则用传入的继承值
float bpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
if (bpm <= 30f)
{
// BPM 无效,不启动任何回调
return;
}
this.m_secondsPerBeat = 60 / bpm;
// TimeSig:检查是否有效
bool hasTimeSig = !string.IsNullOrEmpty(container.TimeSig);
if (hasTimeSig)
{
this.m_beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig);
}
// Grid:检查是否有效
bool hasGrid = container.Grid > 0;
if (hasGrid)
{
this.m_barsPerGrid = container.Grid;
}
double delay = this.m_startDspTime - AudioSettings.dspTime;
if (delay > 0)
{
this.m_delayCoroutine = this.m_host.StartCoroutine(DelayedStart(hasTimeSig, hasGrid));
}
else
{
StartCoroutines(hasTimeSig, hasGrid);
}
}
IEnumerator DelayedStart(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());
// BarTimeSig 有效时启动
if (hasTimeSig)
{
this.m_barCoroutine = this.m_host.StartCoroutine(BarCoroutine());
}
// GridTimeSig 和 Grid 都有效时启动
if (hasTimeSig && hasGrid)
{
this.m_gridCoroutine = this.m_host.StartCoroutine(GridCoroutine());
}
}
internal void OnBlendError(MusicContainer container)
{
this.m_blendError = true;
Debug.LogWarning($"[Music System] Blend Container {container.Id} 包含多个不同 BPM 的子 Container,节拍回调已停止。");
}
internal void StopAll()
{
this.m_stopped = true;
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)
{
double now = AudioSettings.dspTime;
if (this.m_blendError || this.m_stopped)
{
return now;
}
double elapsed = now - this.m_startDspTime;
if (elapsed < 0)
{
return this.m_startDspTime;
}
double period = mode switch
{
AlignMode.Beat => this.m_secondsPerBeat,
AlignMode.Bar => this.m_secondsPerBeat * this.m_beatsPerBar,
_ => this.m_secondsPerBeat,
};
long next = (long)(elapsed / period) + 1L;
return this.m_startDspTime + next * period;
}
IEnumerator BeatCoroutine()
{
double elapsed = AudioSettings.dspTime - this.m_startDspTime;
long index = elapsed > 0.05 ? (long)(elapsed / this.m_secondsPerBeat) : 0;
while (true)
{
double nextTime = this.m_startDspTime + index * this.m_secondsPerBeat;
yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02);
while (AudioSettings.dspTime < nextTime)
yield return null;
if (this.m_blendError || this.m_stopped){
yield break;
}
this.m_onBeat?.Invoke(this.m_containerId);
index++;
}
}
IEnumerator BarCoroutine()
{
double elapsed = AudioSettings.dspTime - this.m_startDspTime;
double secondsPerBar = this.m_secondsPerBeat * this.m_beatsPerBar;
long index = elapsed > 0.05 ? (long)(elapsed / secondsPerBar) : 0;
while (true)
{
double nextTime = this.m_startDspTime + index * secondsPerBar;
yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02);
while (AudioSettings.dspTime < nextTime)
yield return null;
if (this.m_blendError || this.m_stopped){
yield break;
}
this.m_onBar?.Invoke(this.m_containerId);
index++;
}
}
IEnumerator GridCoroutine()
{
double elapsed = AudioSettings.dspTime - this.m_startDspTime;
double secondsPerGrid = this.m_secondsPerBeat * this.m_beatsPerBar * this.m_barsPerGrid;
long index = elapsed > 0.05 ? (long)(elapsed / secondsPerGrid) : 0;
while (true)
{
double nextTime = this.m_startDspTime + index * secondsPerGrid;
yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02);
while (AudioSettings.dspTime < nextTime)
yield return null;
if (this.m_blendError || this.m_stopped){
yield break;
}
this.m_onGrid?.Invoke(this.m_containerId);
index++;
}
}
}
}