From 49a502e647f169273cc6a356ad4f999b3b1b11ad 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 | Bin 0 -> 6348 bytes Assets/Resources/Audios/Bar.wav.meta | 23 ++++ Assets/Resources/Audios/Beat.wav | Bin 0 -> 8894 bytes Assets/Resources/Audios/Beat.wav.meta | 23 ++++ Assets/Resources/Audios/Grid.wav | Bin 0 -> 9174 bytes 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, 353 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 0000000000000000000000000000000000000000..77f0bc3aa362110936eeb48b965800a14762863d GIT binary patch literal 6348 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..73f924ffd3fcdc25057fca0ab29f3d8be3609941 GIT binary patch literal 9174 zcmeHMe{3699e;MR^!HX4s#Ji;;WFs9Uhd9z9G`37l*CSBDNao6HUTzS=eu)qB>sVY z&X2MpCjJ=`f1qhYR167C2qDB!5g>j{{16gCngEFvq!tD(-MUWDwN0x~;l_S^@6PA5 zV_z_B5HNgC^}T!F@8|P<@AG~4-ph45JT~_0LkP_bp9rPW3b7v{WP=j?20~BI+K?UX zMXm0NE&S=zf4F@9v*#CnFTeV+3*)byzp{4qiJLcWf1>;w`!9El?R{wfhkAc-AlBD^ zu>WA9@7jSs_eS3_bN_>TzPzKq`|BuCj+MT2dvd*S>zy|}Yp>qexbfWD>6@inUt53X z_E-P@${l;2J#TbB*X6TcM4!VaDr|XV zqj=|BX`#fF9xg>oFO=SO=i7JYHpnunoU6QuPog2)C7WXZi+!}~RM+XQ&vz|#ab0iO zKV$E+f6>-wdj>Rk7CZ2dD)SXbWvzU+{EPCh%9qRgDkGJrDlb?1u#8{AyU}rU8vPFS z*hpL0Hfwv_cH9=U9k%VT{Slo(pF)%9-RLU*G5!Kp@C+WoPW%WI1_$sY7V#(X^Z5Jt z9A3x0=m-j-45;E96OZm zO+{_9t46p~9+j!N=oa-?_uT5&^tG;Sg*R;0Xwzc&THj4y>zXy%v{<7}i|K7uGkvXV zR@nM>YqVjh_qM7VzFXa`u=VX*;dNWRw{?AM9&21HODnEce%s$Ract&9(uTD4yZ6}o z<%>tRKSqa9RUj#?C?7Evum597CbDy~Kt%J|yVSr*J3S=rCTSPr^*LFei@pC_x~wpb z;mXYf(t52iz{RNEex`cg`*eR#y`O4tMEtKk$+MA(Cwma`Kem!yB$lLnLC$6Z4rYLM z5K>0SigIQy;5Z&1BfXBtxZVDdnK3|c{#Y~=3UdB7j&6es-27CvlOv(oIH&eJF*O}N7M>iAPfc^R zDdnd&SsFaUQYP8b7+F8L`)(q0j8c3}rrbA<)>5!`3#MDepGqr0i z%&lPKptmYb3y5aYFegIOG3YVJ=zhkdW;+f^2(wq#t z(Dj#o;vi%(;FuYV(2?AzG%t^z%uBJ8lX2nXg20N}mbBtqN$1j%!V|ECWeUEPfP)va zNy!IwwV84dnv1d!a15&kVm1;bMzeW|7#d(m0Two5h#g4MDQ{9@j}WlZGZf>bXfMgI zK8Eo*ordNPZZ}l@c`@ai4vtmh!BfC7uP8YmMJ+Ec4=lR|viUiRVOf@!%6~!yeTp=`C`LcD)YQLMY6g4}SZ~!& zxj}U%nn&h19E06nALD(L_WG)mv(eW~sm=p`c%)X_Vlh1%IU*5XI8#vgj3B|e7CJW| z%c3tO46!25vZQCo>n7c9SBm64v_P`%K@tA3G%xVl5F6o5)WNJ!RM(vr?opp$fa+!ENt|5rn_plJ#ITOZ_$3K6XlOG#WiF|@gWR0H_B3vSTgORyf+immaEiu5MZgYA@<`RyI%L?c}a;b%NBSXs6PV zs~weUJ{`GgQnXX)$kmQYHJ^@LH7VMubmVGBrJB!Ha_xry9jbnrlQQr*ZW+G2p??5r C$0mUQ literal 0 HcmV?d00001 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