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; readonly Action 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 onBeat, Action onBar, Action 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) { StopAll(); this.m_blendError = this.m_stopped = false; this.m_containerId = container.Id; this.m_startDspTime = dspTime; // 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; } // 分层启动协程 // 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_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; } internal double GetNextDspTime(AlignMode mode) { double now = AudioSettings.dspTime; if (this.m_blendError || this.m_stopped) { return now; } double elapsed = now - 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++; } } } }