diff --git a/Assets/Resources/AudioData/MusicContainer.bytes b/Assets/Resources/AudioData/MusicContainer.bytes index 0daf0bd..3d77648 100644 Binary files a/Assets/Resources/AudioData/MusicContainer.bytes and b/Assets/Resources/AudioData/MusicContainer.bytes differ diff --git a/Assets/Resources/AudioData/MusicPath.bytes b/Assets/Resources/AudioData/MusicPath.bytes index 7d6c812..e32b95b 100644 Binary files a/Assets/Resources/AudioData/MusicPath.bytes and b/Assets/Resources/AudioData/MusicPath.bytes differ diff --git a/Assets/Resources/AudioData/MusicTransition.bytes b/Assets/Resources/AudioData/MusicTransition.bytes index 16b13d2..b51331c 100644 Binary files a/Assets/Resources/AudioData/MusicTransition.bytes and b/Assets/Resources/AudioData/MusicTransition.bytes differ diff --git a/Assets/Resources/Audios/music_game.wav b/Assets/Resources/Audios/music_game.wav index 856f1aa..090c080 100644 --- a/Assets/Resources/Audios/music_game.wav +++ b/Assets/Resources/Audios/music_game.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:624b5a6e24d135b04b204e09d3b79d316c5be5a20bb3a2415146706a3236c694 -size 11294952 +oid sha256:60b61deb13985d5a0350bae44040664476ca704a7abc7500371265e94ce11a12 +size 11088610 diff --git a/Assets/Resources/Audios/music_home.wav b/Assets/Resources/Audios/music_home.wav index 0e703b1..b01d182 100644 --- a/Assets/Resources/Audios/music_home.wav +++ b/Assets/Resources/Audios/music_home.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78deb96f3d86e131b315c79f5652f83334375b788a28308e93b3f289c56f3a65 -size 11294952 +oid sha256:d567e4b81bf4167eea0dc931f590cc829d5856494084e84eaf0e9aff9d2a6322 +size 11088610 diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 2a16d27..e8da5c0 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -2579,6 +2579,7 @@ GameObject: m_Component: - component: {fileID: 2093584671} - component: {fileID: 2093584670} + - component: {fileID: 2093584672} m_Layer: 0 m_Name: AudioSystem m_TagString: Untagged @@ -2613,6 +2614,18 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2093584672 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2093584669} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a3d8ad76f12545caa287f26889c674e3, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs index 4bb3330..0088bca 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs @@ -244,7 +244,7 @@ namespace OCES.Audio { // ── 启动默认音乐与环境音 ── // 触发一次初始状态,让音乐系统从默认状态开始匹配 - //SetState(GameState.Home); + SetState(GameState.Home); } AudioObject ResolveSwitchContainer(AudioObject switchContainer) diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs index af0f2e0..b715214 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs @@ -65,15 +65,16 @@ namespace OCES.Audio IEnumerator BeatCoroutine() { - long index = 0; // 从第 1 拍开始,第 0 拍是起始点本身 + 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; } @@ -84,12 +85,14 @@ namespace OCES.Audio IEnumerator BarCoroutine() { - long index = 0; + 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 * this.m_secondsPerBeat * this.m_beatsPerBar; + 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){ @@ -102,12 +105,14 @@ namespace OCES.Audio IEnumerator GridCoroutine() { - long index = 0; + 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 * this.m_secondsPerBeat * this.m_beatsPerBar * this.m_barsPerGrid; + 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){ diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs index 501a4a6..aa396c4 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs @@ -6,6 +6,18 @@ using DG.Tweening; namespace OCES.Audio { + /// + /// SyncPoint 协调状态,在 FadeOutBranch 和 FadeInBranch 之间共享。 + /// + internal class SyncPointState + { + public SyncPoint Mode; + public int BaseTimeSamples; // 读取旧 Source 时的 timeSamples + public double BaseDspTime; // 读取旧 Source 时的 dspTime + public int SampleRate; // 旧 Source 的采样率 + public bool Ready; + } + /// /// 与Transition无关的音量/生命周期管理逻辑 /// @@ -47,16 +59,20 @@ namespace OCES.Audio /// /// 淡出分支:fire-and-forget,由调用方 StartCoroutine /// - internal IEnumerator FadeOutBranch(ContainerPlayHandle outgoingHandle, float outgoingVolume, ITransitionConfig transition) + internal IEnumerator FadeOutBranch( + ContainerPlayHandle outgoingHandle, + float outgoingVolume, + ITransitionConfig transition, + SyncPointState syncState = null) { if (outgoingHandle == null) yield break; - + if (transition?.FadeOutOffset > 0f) { //Debug.Log($"Waiting for {transition.FadeOutOffset} to fade out."); yield return new WaitForSeconds(transition.FadeOutOffset); } - + if (transition?.FadeOutTime > 0f ) yield return this.m_coroutineHost.StartCoroutine( FadeOut(outgoingHandle, outgoingVolume, transition.FadeOutTime)); @@ -68,23 +84,52 @@ namespace OCES.Audio /// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。 /// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。 /// - internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, Action onContainerStarted = null) + internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, SyncPointState syncState = null, Action onContainerStarted = null) { if (transition?.FadeInOffset > 0f) { //Debug.Log($"Waiting {transition.FadeInOffset} to fade in."); yield return new WaitForSeconds(transition.FadeInOffset); } - + if (newContainerId == 0) { CurrentHandle = null; CurrentContainerId = 0; + if (syncState != null) syncState.Ready = true; yield break; } - + float startVolume = transition?.FadeInTime > 0f ? 0f : 1f; StartNew(newContainerId, startVolume); + + // SyncPoint: 获取新 Segment 的 AudioSource 并设置 timeSamples + if (syncState is { Mode: SyncPoint.SameAsCurrentSegment }) + { + AudioSource newSource = CurrentHandle?.GetFirstLeafSource(); + if (newSource != null && newSource.clip != null) + { + // 计算从读取旧 Source 到现在经过的 samples + double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime; + int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate); + int targetTimeSamples = syncState.BaseTimeSamples + elapsedSamples; + + if (targetTimeSamples < newSource.clip.samples) + { + newSource.timeSamples = targetTimeSamples; + } + else + { + Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 新音频 samples({newSource.clip.samples}) <= 目标 samples({targetTimeSamples}),降级为 Start"); + } + } + else + { + Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 未能获取新 Segment 的 AudioSource,降级为 Start"); + } + syncState.Ready = true; + } + onContainerStarted?.Invoke(); if (transition?.FadeInTime > 0f) diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs index fb07dd5..0310c9b 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs @@ -47,8 +47,7 @@ namespace OCES.Audio 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; + player.OnBlendError += this.m_beatClock.OnBlendError; } // ───────────────────────────────────────────── @@ -103,7 +102,46 @@ namespace OCES.Audio this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine); this.m_currentFadeOutCoroutine = null; } - + + // ── 构建 SyncPoint 状态 ── + SyncPointState syncState = new() + { + Mode = transition?.SyncPoint ?? SyncPoint.Start, + Ready = false, + }; + + //Debug.Log($"[MusicChannelPlayer] DoTransition: transition?.SyncPoint={transition?.SyncPoint}, Mode={syncState.Mode}"); + + if (syncState.Mode == SyncPoint.SameAsCurrentSegment) + { + // 旧 Container 不存在(如游戏首次启动),静默降级为 Start + if (this.m_fader.CurrentHandle == null) + { + syncState.Mode = SyncPoint.Start; + } + // Blend 类型不支持 SameAsCurrentSegment,降级为 Start + else if (this.m_currentContainer is { ContainerType: ContainerType.Blend }) + { + Debug.LogWarning($"[MusicChannelPlayer] SyncPoint.SameAsCurrentSegment 不支持 Blend 类型 Container({this.m_currentContainer.Id}),降级为 Start"); + syncState.Mode = SyncPoint.Start; + } + else + { + AudioSource oldSource = this.m_fader.CurrentHandle.GetFirstLeafSource(); + if (oldSource && oldSource.clip) + { + syncState.BaseTimeSamples = oldSource.timeSamples; + syncState.BaseDspTime = AudioSettings.dspTime; + syncState.SampleRate = oldSource.clip.frequency; + } + else + { + Debug.LogWarning("[MusicChannelPlayer] SameAsCurrentSegment: 旧 Source 不存在,降级为 Start"); + syncState.Mode = SyncPoint.Start; + } + } + } + // ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)── if (transition != null && this.m_currentContainer != null) { @@ -112,22 +150,39 @@ namespace OCES.Audio } // ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ── - if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break; + if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break; // 如果等待期间被切回来了,就不淡变了 - + ContainerPlayHandle outgoing = this.m_fader.CurrentHandle; float outVol = this.m_fader.CurrentVolume; this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine( - this.m_fader.FadeOutBranch(outgoing, outVol, transition)); + this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState)); this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine( - this.m_fader.FadeInBranch(newContainerId, transition, + this.m_fader.FadeInBranch(newContainerId, transition, syncState, onContainerStarted: () => { - // Music 独有:记录新 container 和播放起始时间 - this.m_currentContainer = this.m_containerConfig.QueryById(newContainerId); - this.m_playStartTime = AudioSettings.dspTime; + MusicContainer container = this.m_containerConfig.QueryById(newContainerId); + float bpm = container.Bpm > 0f ? container.Bpm : 120f; + + // SyncPoint: BeatClock 用调整后的 dspTime,对齐到音频实际播放位置 + double dspTime; + if (syncState is { Mode: SyncPoint.SameAsCurrentSegment }) + { + double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime; + int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate); + double audioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate; + dspTime = AudioSettings.dspTime - audioTime; + } + else + { + dspTime = AudioSettings.dspTime; + } + + this.m_currentContainer = container; + this.m_playStartTime = dspTime; + this.m_beatClock.Restart(container, bpm, dspTime); })); yield return this.m_currentFadeInCoroutine; this.m_currentFadeInCoroutine = null; @@ -147,23 +202,28 @@ namespace OCES.Audio double secondsPerBeat = 60.0 / container.Bpm; - if (mode == AlignMode.Beat) + switch (mode) { - double beatsElapsed = elapsed / secondsPerBeat; - double nextBeat = System.Math.Ceiling(beatsElapsed); - double waitSeconds = (nextBeat - beatsElapsed) * secondsPerBeat; - if (waitSeconds > 0.001) - yield return new WaitForSeconds((float)waitSeconds); - } - else if (mode == AlignMode.Bar) - { - int beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig); - double secondsPerBar = secondsPerBeat * beatsPerBar; - double barsElapsed = elapsed / secondsPerBar; - double nextBar = System.Math.Ceiling(barsElapsed); - double waitSeconds = (nextBar - barsElapsed) * secondsPerBar; - if (waitSeconds > 0.001) - yield return new WaitForSeconds((float)waitSeconds); + case AlignMode.Beat: + { + double beatsElapsed = elapsed / secondsPerBeat; + double nextBeat = Math.Ceiling(beatsElapsed); + double waitSeconds = (nextBeat - beatsElapsed) * secondsPerBeat; + if (waitSeconds > 0.001) + yield return new WaitForSeconds((float)waitSeconds); + break; + } + case AlignMode.Bar: + { + int beatsPerBar = MusicContainerConfig.GetBeatsPerBar(container.TimeSig); + double secondsPerBar = secondsPerBeat * beatsPerBar; + double barsElapsed = elapsed / secondsPerBar; + double nextBar = Math.Ceiling(barsElapsed); + double waitSeconds = (nextBar - barsElapsed) * secondsPerBar; + if (waitSeconds > 0.001) + yield return new WaitForSeconds((float)waitSeconds); + break; + } } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs index 2949311..4150b1c 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainerPlayer.cs @@ -411,5 +411,20 @@ namespace OCES.Audio foreach (ContainerPlayHandle child in this.ChildHandles) child.CollectActiveSources(result); } + + /// + /// 获取第一个正在播放的 Leaf Segment 的 AudioSource。 + /// 递归遍历子句柄,优先取当前层级的 ActiveSources,找不到再深入子层级。 + /// + internal AudioSource GetFirstLeafSource() + { + if (this.ActiveSources.Count > 0) return this.ActiveSources[0]; + foreach (ContainerPlayHandle child in this.ChildHandles) + { + AudioSource src = child.GetFirstLeafSource(); + if (src) return src; + } + return null; + } } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/SfxSystem.cs b/Assets/Scripts/OCES/Audio/HandWritten/SfxSystem.cs index 751fe10..e850127 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/SfxSystem.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/SfxSystem.cs @@ -55,7 +55,7 @@ namespace OCES.Audio { this.m_clipConcurrentCount[id] = GetClipCount(id) + 1; #if UNITY_EDITOR - Debug.Log($"{id} count added to {GetClipCount(id)}"); + //Debug.Log($"{id} count added to {GetClipCount(id)}"); #endif } diff --git a/Assets/Scripts/OCES/SetStateBind.cs b/Assets/Scripts/OCES/SetStateBind.cs index 9b87d82..5d50705 100644 --- a/Assets/Scripts/OCES/SetStateBind.cs +++ b/Assets/Scripts/OCES/SetStateBind.cs @@ -7,7 +7,7 @@ namespace OCES { public class SetStateBind : MonoBehaviour { - public TileMaterial targetGameState; + public GameState targetGameState; public bool enableLowpass; public Text buttonText;