From 69944f44ea5318b6494e4efe4c02c7ec959bc1f6 Mon Sep 17 00:00:00 2001 From: Oliver Wong Date: Tue, 7 Apr 2026 10:06:02 +0800 Subject: [PATCH] WIP: Music callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头几拍会抖动一下,导致对不上拍子 --- Assets/Resources/AudioData/AudioObject.bytes | Bin 7567 -> 7707 bytes .../Resources/AudioData/MusicContainer.bytes | Bin 107 -> 110 bytes .../Resources/AudioData/MusicTransition.bytes | Bin 29 -> 29 bytes Assets/Resources/Audios/Bar.wav | 3 + Assets/Resources/Audios/Bar.wav.meta | 23 ++++ Assets/Resources/Audios/Beat.wav | 3 + Assets/Resources/Audios/Beat.wav.meta | 23 ++++ Assets/Resources/Audios/Grid.wav | 3 + Assets/Resources/Audios/Grid.wav.meta | 23 ++++ .../Audio/Generated/AudioObjectDefinitions.cs | 3 + .../OCES/Audio/HandWritten/AudioSystem.cs | 9 ++ .../HandWritten/HandWrittenDefinitions.cs | 15 ++ .../LongAudio/AmbienceChannelPlayer.cs | 6 +- .../Audio/HandWritten/LongAudio/BeatClock.cs | 129 ++++++++++++++++++ .../HandWritten/LongAudio/BeatClock.cs.meta | 3 + .../HandWritten/LongAudio/ChannelFader.cs | 4 +- .../LongAudio/MusicChannelPlayer.cs | 42 +++--- .../LongAudio/MusicContainerPlayer.cs | 86 +++++++----- .../HandWritten/LongAudio/MusicSystem.cs | 18 ++- Assets/Scripts/OCES/CallBackTest.cs | 27 ++++ Assets/Scripts/OCES/CallBackTest.cs.meta | 3 + 21 files changed, 362 insertions(+), 61 deletions(-) create mode 100644 Assets/Resources/Audios/Bar.wav create mode 100644 Assets/Resources/Audios/Bar.wav.meta create mode 100644 Assets/Resources/Audios/Beat.wav create mode 100644 Assets/Resources/Audios/Beat.wav.meta create mode 100644 Assets/Resources/Audios/Grid.wav create mode 100644 Assets/Resources/Audios/Grid.wav.meta create mode 100644 Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs create mode 100644 Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs.meta create mode 100644 Assets/Scripts/OCES/CallBackTest.cs create mode 100644 Assets/Scripts/OCES/CallBackTest.cs.meta diff --git a/Assets/Resources/AudioData/AudioObject.bytes b/Assets/Resources/AudioData/AudioObject.bytes index e7a67fb201226a0f15b053eb115b978127c4750b..25220d6c3ffc49bd6a99ecaa02de8a10262a9ca5 100644 GIT binary patch delta 65 zcmeCTo^8XJwvmyMm&b&Gfq@Z-nVk}gCI%|;nnL+3PN|6{AOSO&fO}D93Q%A(6K}5o E0DaZla_t3j+{Lv^4|(ExZIf diff --git a/Assets/Resources/AudioData/MusicTransition.bytes b/Assets/Resources/AudioData/MusicTransition.bytes index a18a9fc7a39ed25a0347db57342feaec168c0ea2..68f1c4ca1fea80dca9fbff1030af67ca62fabeed 100644 GIT binary patch literal 29 RcmZQ%U|_I!;s=sg000f@0FnRz literal 29 WcmZQ%U|_I!;s=rq_CNxO1~LI7&H@ks diff --git a/Assets/Resources/Audios/Bar.wav b/Assets/Resources/Audios/Bar.wav new file mode 100644 index 0000000..b272517 --- /dev/null +++ b/Assets/Resources/Audios/Bar.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559f3320d62ee21cf8d8f9c3e398635bad363bc628cf0e2b5292b57e91897e9d +size 6348 diff --git a/Assets/Resources/Audios/Bar.wav.meta b/Assets/Resources/Audios/Bar.wav.meta new file mode 100644 index 0000000..9c0da18 --- /dev/null +++ b/Assets/Resources/Audios/Bar.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 24597b5de367d47a6a05f83442f5150c +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Audios/Beat.wav b/Assets/Resources/Audios/Beat.wav new file mode 100644 index 0000000..1b21601 --- /dev/null +++ b/Assets/Resources/Audios/Beat.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f52a70cd78d533b217909b7e8722e024b5d0a20440be2ba73badc0a82fbbde +size 8894 diff --git a/Assets/Resources/Audios/Beat.wav.meta b/Assets/Resources/Audios/Beat.wav.meta new file mode 100644 index 0000000..5c42137 --- /dev/null +++ b/Assets/Resources/Audios/Beat.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: db8d11d4ce8214c5c944ba9c66732b94 +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Audios/Grid.wav b/Assets/Resources/Audios/Grid.wav new file mode 100644 index 0000000..5d8435e --- /dev/null +++ b/Assets/Resources/Audios/Grid.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:067e95c11ee8ed8f7209551a9451e67cd6de24c9a15e4e5729da8cc22dee90d9 +size 9174 diff --git a/Assets/Resources/Audios/Grid.wav.meta b/Assets/Resources/Audios/Grid.wav.meta new file mode 100644 index 0000000..16cea50 --- /dev/null +++ b/Assets/Resources/Audios/Grid.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 762bc1c75d1ed4a02bbf0aba8d774c94 +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/OCES/Audio/Generated/AudioObjectDefinitions.cs b/Assets/Scripts/OCES/Audio/Generated/AudioObjectDefinitions.cs index 51ddc04..1940cba 100644 --- a/Assets/Scripts/OCES/Audio/Generated/AudioObjectDefinitions.cs +++ b/Assets/Scripts/OCES/Audio/Generated/AudioObjectDefinitions.cs @@ -66,6 +66,9 @@ public static class AudioObjectDefinitions { "Chinese Number 08", 49 }, { "Chinese Number 09", 49 }, { "Chinese Number 10", 49 }, + { "Bar", 52 }, + { "Beat", 53 }, + { "Grid", 54 }, { "sfx_amb_desert", 2000 }, { "sfx_amb_forest", 2001 }, { "sfx_anim_common_item_fly", 3000 }, diff --git a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs index fa09d5d..e29430c 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs @@ -26,6 +26,11 @@ namespace OCES.Audio // 公开接口 // ───────────────────────────────────────────── + public event Action OnBeat; + + public event Action OnBar; + public event Action OnGrid; + public void Play(uint audioId, Action onPlay = null) { AudioObject obj = this.m_audioObjects.QueryById(audioId); @@ -170,6 +175,10 @@ namespace OCES.Audio musicPool, ambiencePool); + this.m_musicSystem.OnBeat += id => this.OnBeat?.Invoke(id); + this.m_musicSystem.OnBar += id => this.OnBar?.Invoke(id); + this.m_musicSystem.OnGrid += id => this.OnGrid?.Invoke(id); + // ── 注册 StateGroup ── EnumIds.RegisterAllGameState(); diff --git a/Assets/Scripts/OCES/Audio/HandWritten/HandWrittenDefinitions.cs b/Assets/Scripts/OCES/Audio/HandWritten/HandWrittenDefinitions.cs index c24ea55..8d13bc8 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/HandWrittenDefinitions.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/HandWrittenDefinitions.cs @@ -84,4 +84,19 @@ namespace OCES.Audio public partial class MusicPath : IPathEntry { } public partial class AmbiencePath : IPathEntry { } + + public partial class MusicContainerConfig + { + /// + /// 解析拍号字符串(如 "4/4", "3/4"),返回每小节拍数。 + /// + public static int GetBeatsPerBar(string timeSig) + { + if (string.IsNullOrEmpty(timeSig)) return 4; + string[] parts = timeSig.Split('/'); + if (parts.Length >= 1 && int.TryParse(parts[0], out int beats)) + return beats; + return 4; + } + } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/AmbienceChannelPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/AmbienceChannelPlayer.cs index 25438d3..e933b87 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/AmbienceChannelPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/AmbienceChannelPlayer.cs @@ -7,7 +7,7 @@ namespace OCES.Audio /// 环境音通道播放器。 /// 与 MusicChannelPlayer 逻辑相同,但使用 AmbienceTransition 表,不涉及节拍对齐。 /// - public class AmbienceChannelPlayer + class AmbienceChannelPlayer { readonly AmbienceTransitionConfig m_transitionConfig; readonly MonoBehaviour m_coroutineHost; @@ -20,7 +20,7 @@ namespace OCES.Audio uint m_currentContainerId; - public AmbienceChannelPlayer( + internal AmbienceChannelPlayer( AmbienceTransitionConfig transitionConfig, MusicContainerPlayer player, MonoBehaviour coroutineHost) @@ -34,7 +34,7 @@ namespace OCES.Audio // 公开接口 // ───────────────────────────────────────────── - public void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId) + internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId) { if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null) return; diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs new file mode 100644 index 0000000..f3155d3 --- /dev/null +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs @@ -0,0 +1,129 @@ +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; + } + + public void Restart(MusicContainer container, float inheritedBpm, double dspTime) + { + Debug.Log($"[BeatClock] Restart called, container={container.Id}, bpm={container.Bpm}, inherited={inheritedBpm}"); + + StopAll(); + this.m_blendError = this.m_stopped = false; + this.m_containerId = container.Id; + this.m_startDspTime = dspTime; + + + // BPM:优先用自己的,为 0 则用传入的继承值,最终回退 120 + float bpm = container.Bpm > 0f ? container.Bpm : inheritedBpm; + if (bpm <= 30f) bpm = 120f; // 没见过速度小于30bpm的音乐,小于30要么是数填错了,要么是有奇怪的情况。要是真有这么慢的音乐再处理吧。 + this.m_secondsPerBeat = 60 / bpm; + + this.m_beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig); // 沿用现有的解析逻辑 + this.m_barsPerGrid = container.Grid > 0 ? container.Grid : 4; + + this.m_beatCoroutine = this.m_host.StartCoroutine(BeatCoroutine()); + this.m_barCoroutine = this.m_host.StartCoroutine(BarCoroutine()); + 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; + } + + IEnumerator BeatCoroutine() + { + long index = 0; // 从第 1 拍开始,第 0 拍是起始点本身 + while (true) + { + double nextTime = this.m_startDspTime + index * this.m_secondsPerBeat; + if (index == 0) + { + Debug.Log($"[BeatClock] BeatCoroutine waiting, nextTime={nextTime}, now={AudioSettings.dspTime}"); + } + yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02); + + while (AudioSettings.dspTime < nextTime) + yield return null; + + if (this.m_blendError || this.m_stopped){ + Debug.Log($"[BeatClock] Coroutine exiting early, blendError={m_blendError}, stopped={m_stopped}"); + yield break; + } + this.m_onBeat?.Invoke(this.m_containerId); + Debug.Log($"[Beat] index={index}, nextTime={nextTime:F4}, actualDspTime={AudioSettings.dspTime:F4}, diff={(AudioSettings.dspTime - nextTime)*1000:F1}ms"); + index++; + } + } + + IEnumerator BarCoroutine() + { + long index = 0; + while (true) + { + double nextTime = this.m_startDspTime + index * this.m_secondsPerBeat * this.m_beatsPerBar; + yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02); + + while (AudioSettings.dspTime < nextTime) + yield return null; + if (this.m_blendError || this.m_stopped){ + Debug.Log($"[BeatClock] Coroutine exiting early, blendError={m_blendError}, stopped={m_stopped}"); + yield break; + } + this.m_onBar?.Invoke(this.m_containerId); + index++; + } + } + + IEnumerator GridCoroutine() + { + long index = 0; + while (true) + { + double nextTime = this.m_startDspTime + index * this.m_secondsPerBeat * this.m_beatsPerBar * this.m_barsPerGrid; + yield return new WaitUntil(() => AudioSettings.dspTime >= nextTime - 0.02); + + while (AudioSettings.dspTime < nextTime) + yield return null; + if (this.m_blendError || this.m_stopped){ + Debug.Log($"[BeatClock] Coroutine exiting early, blendError={m_blendError}, stopped={m_stopped}"); + yield break; + } + this.m_onGrid?.Invoke(this.m_containerId); + index++; + } + } + } +} diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs.meta b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs.meta new file mode 100644 index 0000000..2ecd1e9 --- /dev/null +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5cb2b4c3391444bca4cd30f3015d0f91 +timeCreated: 1775200354 \ No newline at end of file diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs index 5f3e2d6..501a4a6 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs @@ -14,11 +14,11 @@ namespace OCES.Audio readonly MusicContainerPlayer m_player; readonly MonoBehaviour m_coroutineHost; - public ContainerPlayHandle CurrentHandle { get; private set; } + internal ContainerPlayHandle CurrentHandle { get; private set; } public uint CurrentContainerId { get; private set; } public float CurrentVolume { get; private set; } - public ChannelFader(MusicContainerPlayer player, MonoBehaviour coroutineHost) + internal ChannelFader(MusicContainerPlayer player, MonoBehaviour coroutineHost) { this.m_player = player; this.m_coroutineHost = coroutineHost; diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs index f2574cb..fb07dd5 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using UnityEngine; @@ -7,12 +8,13 @@ namespace OCES.Audio /// 音乐通道播放器。 /// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。 /// - public class MusicChannelPlayer + class MusicChannelPlayer { readonly MusicContainerConfig m_containerConfig; readonly MusicTransitionConfig m_transitionConfig; readonly MonoBehaviour m_coroutineHost; readonly ChannelFader m_fader; + readonly BeatClock m_beatClock; Coroutine m_currentFadeInCoroutine; Coroutine m_currentFadeOutCoroutine; @@ -30,16 +32,23 @@ namespace OCES.Audio // 正在进行的 transition 协程(防止重叠) Coroutine m_transitionCoroutine; - public MusicChannelPlayer( + internal MusicChannelPlayer( MusicContainerConfig containerConfig, MusicTransitionConfig transitionConfig, MusicContainerPlayer player, - MonoBehaviour coroutineHost) + MonoBehaviour coroutineHost, + Action onBeat, + Action onBar, + Action onGrid) { - this.m_containerConfig = containerConfig; - this.m_transitionConfig = transitionConfig; - this.m_coroutineHost = coroutineHost; - this.m_fader = new ChannelFader(player, coroutineHost); + this.m_containerConfig = containerConfig; + this.m_transitionConfig = transitionConfig; + this.m_coroutineHost = coroutineHost; + 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; } // ───────────────────────────────────────────── @@ -67,8 +76,10 @@ namespace OCES.Audio /// /// 立即停止当前播放(无淡出) /// - public void Stop() + internal void Stop() { + this.m_beatClock.StopAll(); + if (this.m_transitionCoroutine != null) this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine); @@ -146,7 +157,7 @@ namespace OCES.Audio } else if (mode == AlignMode.Bar) { - int beatsPerBar = ParseBeatsPerBar(container.TimeSig); + int beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig); double secondsPerBar = secondsPerBeat * beatsPerBar; double barsElapsed = elapsed / secondsPerBar; double nextBar = System.Math.Ceiling(barsElapsed); @@ -155,18 +166,7 @@ namespace OCES.Audio yield return new WaitForSeconds((float)waitSeconds); } } - - /// - /// 解析拍号字符串(如 "4/4", "3/4"),返回每小节拍数。 - /// - static int ParseBeatsPerBar(string timeSig) - { - if (string.IsNullOrEmpty(timeSig)) return 4; - string[] parts = timeSig.Split('/'); - if (parts.Length >= 1 && int.TryParse(parts[0], out int beats)) - return beats; - return 4; - } + // ───────────────────────────────────────────── // 工具 // ───────────────────────────────────────────── diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs index 558f8a1..cf2d4fb 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs @@ -1,7 +1,9 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; +using Random = UnityEngine.Random; namespace OCES.Audio { @@ -10,7 +12,7 @@ namespace OCES.Audio /// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。 /// 不持有状态机逻辑,只负责"把这个 Container 播完"。 /// - public class MusicContainerPlayer + class MusicContainerPlayer { readonly MusicContainerConfig m_containerConfig; readonly MusicSegmentConfig m_segmentConfig; @@ -18,6 +20,12 @@ namespace OCES.Audio readonly MonoBehaviour m_coroutineHost; UnityEngine.Audio.AudioMixerGroup m_mixerGroup; + /// + /// 开始播放Container时触发 音乐回调系统 + /// container本身,继承来的bpm,进入时刻的dspTime + /// + internal event Action OnContainerEntered; + internal event Action OnBlendError; // Sequence Step 模式的全局游标,key = containerId readonly Dictionary m_sequenceStepIndex = new(); @@ -45,7 +53,7 @@ namespace OCES.Audio /// 开始播放指定 Container,播放完毕后调用 onFinished。 /// 返回可用于外部停止的句柄。 /// - public ContainerPlayHandle Play(uint containerId, System.Action onFinished = null) + internal ContainerPlayHandle Play(uint containerId, Action onFinished = null, float inheritedBpm = 0f) { MusicContainer container = this.m_containerConfig.QueryById(containerId); if (container == null) @@ -58,7 +66,7 @@ namespace OCES.Audio ContainerPlayHandle handle = new(); handle.TargetVolume = 1f; handle.Coroutine = this.m_coroutineHost.StartCoroutine( - PlayContainerCoroutine(container, handle, onFinished)); + PlayContainerCoroutine(container, handle, inheritedBpm, onFinished)); return handle; } @@ -93,14 +101,19 @@ namespace OCES.Audio IEnumerator PlayContainerCoroutine( MusicContainer container, ContainerPlayHandle handle, - System.Action onFinished) + float inheritedBpm, + Action onFinished) { + float effectiveBpm = container.Bpm > 0f ? container.Bpm : inheritedBpm; + Debug.Log($"[MusicContainerPlayer] OnContainerEntered firing, container={container.Id}, subscribers={OnContainerEntered != null}"); + OnContainerEntered?.Invoke(container, effectiveBpm, AudioSettings.dspTime); + int loopsCompleted = 0; while (true) { yield return this.m_coroutineHost.StartCoroutine( - PlayContainerOnce(container, handle.TargetVolume, handle)); + PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm)); if (handle.Cancelled) yield break; @@ -126,7 +139,7 @@ namespace OCES.Audio /// /// 播放一个 Container 一轮(不含循环逻辑) /// - IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle) + IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm) { if (container.Segments == null || container.Segments.Count == 0) yield break; @@ -134,15 +147,15 @@ namespace OCES.Audio switch (container.ContainerType) { case ContainerType.Blend: - yield return PlayBlend(container, volumeScale, handle); + yield return PlayBlend(container, volumeScale, handle, inheritedBpm); break; case ContainerType.Sequence: - yield return PlaySequence(container, volumeScale, handle); + yield return PlaySequence(container, volumeScale, handle, inheritedBpm); break; case ContainerType.Random: - yield return PlayRandom(container, volumeScale, handle); + yield return PlayRandom(container, volumeScale, handle, inheritedBpm); break; } } @@ -151,10 +164,21 @@ namespace OCES.Audio // Blend(同时播放) // ───────────────────────────────────────────── - IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle) + IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm) { + IEnumerable tempos = container.Segments.Select(id => + { + MusicContainer c = this.m_containerConfig.QueryById(id); + return c?.Bpm ?? 0f; + }).Where(b => b > 0f).Distinct(); + + if (tempos.Count() > 1) + { + OnBlendError?.Invoke(container); + } + // 同时启动所有子元素,等待全部结束 - var childHandles = new List(); + List childHandles = new(); bool allDone = false; int remaining = container.Segments.Count; @@ -166,7 +190,7 @@ namespace OCES.Audio { remaining--; if (remaining <= 0) allDone = true; - }); + }, inheritedBpm); if (childHandle != null) { childHandles.Add(childHandle); @@ -187,7 +211,7 @@ namespace OCES.Audio // Sequence // ───────────────────────────────────────────── - IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle) + IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm) { bool isStep = container.ContainerPlayMode; @@ -195,7 +219,7 @@ namespace OCES.Audio { // Step: 每次只播一个,游标全局推进 int index = GetNextSequenceIndex(container); - yield return PlayChildAndWait(container.Segments[index], volumeScale, handle); + yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm); } else @@ -209,7 +233,7 @@ namespace OCES.Audio if (i > 0 && container.StrategyParam > 0) yield return new WaitForSeconds(container.StrategyParam); - yield return PlayChildAndWait(container.Segments[i], volumeScale, handle); + yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm); } } } @@ -218,7 +242,7 @@ namespace OCES.Audio // Random // ───────────────────────────────────────────── - IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle) + IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm) { bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous @@ -226,7 +250,7 @@ namespace OCES.Audio { // Step Random: 随机选一个播放,算一次 loopCount uint chosen = PickRandomChild(container); - yield return PlayChildAndWait(chosen, volumeScale, handle); + yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm); } else { @@ -244,7 +268,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); + yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm); } } } @@ -256,10 +280,10 @@ namespace OCES.Audio /// /// 播放一个子元素(segment 或 container),等待其完成后返回。 /// - IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle) + IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle, float inheritedBpm) { bool done = false; - ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true); + ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm); if (child != null) parentHandle.ChildHandles.Add(child); @@ -272,17 +296,17 @@ namespace OCES.Audio /// /// 启动一个子元素的播放,不等待,返回句柄。 /// - ContainerPlayHandle PlayChild(uint id, float volumeScale, System.Action onDone) + ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm) { // ID < 1000000 是 MusicSegment,否则是嵌套 Container - return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone); + return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone, inheritedBpm: inheritedBpm); } // ───────────────────────────────────────────── // Segment 播放 // ───────────────────────────────────────────── - ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, System.Action onFinished) + ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished) { MusicSegment segment = this.m_segmentConfig.QueryById(segmentId); if (segment == null) @@ -313,7 +337,7 @@ namespace OCES.Audio return handle; } - IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, System.Action onFinished) + IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, Action onFinished) { yield return new WaitWhile(() => source.isPlaying && !handle.Cancelled); @@ -370,18 +394,18 @@ namespace OCES.Audio /// /// 一次 Container 播放的句柄,用于外部停止或淡出时访问正在播放的 AudioSource。 /// - public class ContainerPlayHandle + class ContainerPlayHandle { - public Coroutine Coroutine; - public bool Cancelled; - public float TargetVolume = 1f; - public List ActiveSources = new(); - public List ChildHandles = new(); + internal Coroutine Coroutine; + internal bool Cancelled; + internal float TargetVolume = 1f; + internal List ActiveSources = new(); + internal List ChildHandles = new(); /// /// 递归收集所有正在发声的 AudioSource(用于淡出) /// - public void CollectActiveSources(List result) + internal void CollectActiveSources(List result) { result.AddRange(this.ActiveSources); foreach (ContainerPlayHandle child in this.ChildHandles) diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSystem.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSystem.cs index 3ce82ad..cd14c61 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSystem.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSystem.cs @@ -7,17 +7,21 @@ namespace OCES.Audio /// 音乐与环境音系统。由 AudioSystem 持有并初始化。 /// 对外只暴露 OnStateChanged,由 AudioSystem.SetState 转发调用。 /// - public class MusicSystem : MonoBehaviour + class MusicSystem : MonoBehaviour { MusicStateRouter m_stateRouter; MusicChannelPlayer m_musicChannel; AmbienceChannelPlayer m_ambienceChannel; + internal event Action OnBeat; + internal event Action OnBar; + internal event Action OnGrid; + // 记录上一次两个通道各自匹配到的 PathId,用于查 Transition 表 uint m_lastMusicPathId; uint m_lastAmbiencePathId; - public void Initialize( + internal void Initialize( MusicSegmentConfig segments, MusicContainerConfig containers, MusicPathConfig musicPaths, @@ -31,14 +35,20 @@ namespace OCES.Audio MusicContainerPlayer ambientContainerPlayer = new(containers, segments, ambiencePool, this); this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths); - this.m_musicChannel = new MusicChannelPlayer(containers, musicTransitions, musicContainerPlayer, this); + this.m_musicChannel = new MusicChannelPlayer( + containers, musicTransitions, musicContainerPlayer, this, + id => OnBeat?.Invoke(id), + id => OnBar?.Invoke(id), + id => OnGrid?.Invoke(id)); this.m_ambienceChannel = new AmbienceChannelPlayer(ambienceTransitions, ambientContainerPlayer, this); + + } /// /// 由 AudioSystem.SetState 调用,更新状态并驱动两个通道切换。 /// - public void OnStateChanged(TEnum state) where TEnum : Enum + internal void OnStateChanged(TEnum state) where TEnum : Enum { this.m_stateRouter.SetState( state, diff --git a/Assets/Scripts/OCES/CallBackTest.cs b/Assets/Scripts/OCES/CallBackTest.cs new file mode 100644 index 0000000..4f25a5b --- /dev/null +++ b/Assets/Scripts/OCES/CallBackTest.cs @@ -0,0 +1,27 @@ +using OCES.Audio; +using UnityEngine; + +namespace OCES +{ + public class CallBackTest : MonoBehaviour + { + void Start() + { + AudioSystem.Instance.OnBeat += u => + { + AudioSystem.Instance.Play(52); + Debug.Log($"Container {u} is OnBeat"); + }; + AudioSystem.Instance.OnBar += u => + { + AudioSystem.Instance.Play(53); + Debug.Log($"Container {u} is OnBar"); + }; + AudioSystem.Instance.OnGrid += u => + { + AudioSystem.Instance.Play(54); + Debug.Log($"Container {u} is OnGrid"); + }; + } + } +} diff --git a/Assets/Scripts/OCES/CallBackTest.cs.meta b/Assets/Scripts/OCES/CallBackTest.cs.meta new file mode 100644 index 0000000..53f5d47 --- /dev/null +++ b/Assets/Scripts/OCES/CallBackTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a3d8ad76f12545caa287f26889c674e3 +timeCreated: 1775210256 \ No newline at end of file