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 c769333..8244d6b 100644
Binary files a/Assets/Resources/Audios/music_game.wav and b/Assets/Resources/Audios/music_game.wav differ
diff --git a/Assets/Resources/Audios/music_home.wav b/Assets/Resources/Audios/music_home.wav
index 185f3df..6959509 100644
Binary files a/Assets/Resources/Audios/music_home.wav and b/Assets/Resources/Audios/music_home.wav differ
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;