205 lines
7.4 KiB
C#
205 lines
7.4 KiB
C#
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(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());
|
||
|
||
// Bar:TimeSig 有效时启动
|
||
if (hasTimeSig)
|
||
{
|
||
this.m_barCoroutine = this.m_host.StartCoroutine(BarCoroutine());
|
||
}
|
||
|
||
// Grid:TimeSig 和 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++;
|
||
}
|
||
}
|
||
}
|
||
}
|