From 2b34d0bf947463d7af4bc2fbdedee4cf066416a1 Mon Sep 17 00:00:00 2001 From: Oliver Wong Date: Thu, 16 Apr 2026 20:50:34 +0800 Subject: [PATCH] feat: SyncPoint.SameAsCurrentSegment music transition - Add SameAsCurrentSegment mode to align new container's timeSamples with the old container's playback position, accounting for FadeInOffset - Fix BeatClock callback burst when Restart is called with a past dspTime - Add GetFirstLeafSource() for resolving playback position across nested containers - Manual BeatClock.Restart replaces OnContainerEntered subscription for accurate timing with SyncPoint --- .../Resources/AudioData/MusicContainer.bytes | Bin 144 -> 170 bytes Assets/Resources/AudioData/MusicPath.bytes | Bin 52 -> 52 bytes .../Resources/AudioData/MusicTransition.bytes | Bin 34 -> 34 bytes Assets/Resources/Audios/music_game.wav | Bin 11294952 -> 11088610 bytes Assets/Resources/Audios/music_home.wav | Bin 11294952 -> 11088610 bytes Assets/Scenes/SampleScene.unity | 13 ++ .../OCES/Audio/HandWritten/AudioSystem.cs | 2 +- .../Audio/HandWritten/LongAudio/BeatClock.cs | 23 ++-- .../HandWritten/LongAudio/ChannelFader.cs | 57 ++++++++- .../LongAudio/MusicChannelPlayer.cs | 112 ++++++++++++++---- .../LongAudio/MusicContainerPlayer.cs | 15 +++ .../OCES/Audio/HandWritten/SfxSystem.cs | 2 +- Assets/Scripts/OCES/SetStateBind.cs | 2 +- 13 files changed, 182 insertions(+), 44 deletions(-) diff --git a/Assets/Resources/AudioData/MusicContainer.bytes b/Assets/Resources/AudioData/MusicContainer.bytes index 0daf0bdd4b3e45764960025471555cbc1dac671a..3d776484e40f6470bb0302451c077f39d6b77b24 100644 GIT binary patch delta 66 ycmbQhxQdaHbs}S-1tS9kg9s3V!2kaY3@4nJP4rDzfIL?xeg=@NC`bwjP-Ovda|(+9 delta 40 ncmZ3*IDwIoWg=st91{Zrg9s3d0x^jF|38pmU^dY=VFA(rc?Jb6 diff --git a/Assets/Resources/AudioData/MusicPath.bytes b/Assets/Resources/AudioData/MusicPath.bytes index 7d6c812cc8844b34146aeff28358cb30f2ebecaf..e32b95b2d0e7ef1dfe9d0b9166ac081d8d8700ad 100644 GIT binary patch literal 52 mcmZQ(U|?VbVrD}f14k!*Fq;X&H*^B>!E6ZM#2LtE2C@NG5(9n! literal 52 lcmZQ(U|?VbVrD}f0~aTLFq;X&Hw5v)YzW`P8OUb_vH@1Q1A_nn diff --git a/Assets/Resources/AudioData/MusicTransition.bytes b/Assets/Resources/AudioData/MusicTransition.bytes index 16b13d2284d0d8a352cbbaade0aa5fd5cc845082..b51331cc5762b82d26b6c0c116f7d7974b85b8ba 100644 GIT binary patch literal 34 acmZQ%U|_I!;s=rq_6Y3Yz`zI;0s{a-lmk!z literal 34 UcmZQ%U|_I!;s=swfDy(A01(XplK=n! diff --git a/Assets/Resources/Audios/music_game.wav b/Assets/Resources/Audios/music_game.wav index c769333b7495896b5e9695bd317bca277653b8b9..8244d6bca72981c058ea0c79ba0a2c49de4145b2 100644 GIT binary patch delta 7363 zcmeHMNp~Yx8C9F^0A@%-0)#LLHA9DvR3%x~gsjGG$!&pU8(G~pVKAjq$u4XS(Jgss zhDrx+m;!{cna2RlIn5%AoGh}Se*nLLCFgJsEBFh%Dyd{mCAZWLt7svY-uv#o-*>-P z^`7K^zwpAjfBx(FbC>ej#l?Sw{^Hq~zn&>q4DWf5$8*Lr<$3C5aq*ui&sooTPf0P9 zvv+Ub1aXF#B3?tBCC(8K5U(X(N4%bxCf-22k$4mFX5vBOJn}7;v(@F@e$$@@i^fp0)#}!M34v( zGenq}B_c$Wh!JrjL41@*5_3d~xJ-PEm?y3f3q+d85R1fBVu{ESA16LREE73mg;*ul zh))uEVx1@u*NE%H2C+$eig<$fH1QeYv&83!E#mWpLKF#=C=nV_Cbo$kLMOgJ>=JuK zg{TrWqE0+XGzej=K{Sbd;($0Lj))t?7l|(sUnagnn8Z`WP2y?dtHjp`63-A{C%!>^ zllT_#ZQ@ztJH&U1?-9=t-zR=R{E+w&@nhmA#4X~d#LtMI6TcvSN&JfVHSrtbx5V#= z-xGfzo+tiD{E7JUi@1I6?u*8qhp$gPG4;~TySJy#+r>2uvHWmdUm0VxT zWYVc*A)Cvr7v^(oTkGO&IkUdCo?ThUY~`})mF(4}!q)O?Di{sLWb2ib2ExrD@h6im zWmCh*`ng0{Qfeo9C9_#bS=n!`=CfC`EAxfbd}_I7ZtJRdtx-!#eN#Q5+0~V;^n4+c z3dx~}Uk>|&kzPP2U;t)SoC-z~!B`?39R!*7yp6K1w(^<9Og>{dxV*a2?VOd~kceT( zC1Jef&k%?`Wn->q^6MgNGZkzFwm-Klx+dC}vRfwJ=H+yHej&0b2c418e}i&5+8v*f z(~<7a@{j+MamY?^uUgl{LO~~`A{)EPz66{s~Pszv$ zY25tZTFY(z^&xv(sw?WQW_XL*wk|fN|GxY;pI0x<`8H;Aa<0Ch?dVH48ru4em4bR> zSB;nKUFtA#SgBVuL-C4Tt%}6qoKGp$idsTkTf>siYlj%-?woJln(=Pt*1QX~hUSe1 zf__yzmb}q;peUDPMJ@igS3LNGQZOXRF@G?g2nG|OkW;ZQH7zcajZ!(0PcOCw#iu#n zj$zajl5}uz5IC3#)Ee7TFdmOfa!3k={361SBeQBKhyE%qv{)RXv<9`I*A1~3do5z6 zSZf+{KA)x3d9_Qc)YG|+Ruuue(12R2NWue2Y~)h>sUT-$` zY@17}r0r=H0f8_eD0R#g-X(=A>0F}F5X%ZFv&Rj)iC2_uyQ3YSEs)iOJ>Bj_U1>nG zlAH56*>&@GU~M785?WCzT56%Gt=06Z;ms+HU2)J#iUyfhF2^A~*|eCtX)INX#zJ<0 z_Mkhk+z{G%#n4jauyqV`u&r^^n&<~7rB=Cv6Q^QB+H0F{+hDeR=u~4n)t`{nf&PkZ z4yPE~!AXoTh5g44_3OT}MyxlBs=24vyVt|`K4hz_O=Dsotn-%x9@mGKJH5Ay!)12| zT?@$SUm_=IL}FI|4-W6K|)cj z?JfmnUn?c0p0VSsM0o4f2I*BeEpR$G%;rH0TKu|sjCU6~*wLz8U;PGFYm3W{awkEp zY#b;JZGKyiSf?@H%sGI$p!OKPHAYnT}2g-g@YK>dN?IqLV?K+X#6RlBk;1$0K zNuA@igK_TC6aTuM|3?G+7xU=oQM04;Pfx`?1JnEeP;nO-Rlj@IE;AF9GJ*aCDlYko zVORZARdLgwcGq`{ z#XZ5X^e0vsOaB#68Ps<~#;Sji=7bSP#XZb$pStInQ7Y5o|4Qv2@U*J<)$Tz2Jm>iz D=gf~? delta 3999 zcmeH~OK(+G6oAj=a-~QW#EV7Ria@2vCsL_^_<$m%&?4p8Ql26xZ53##;2W^wCk2$3 z%0&<;P_Q@YKuAbuq!UJl0TT@a5@KR>NJ5wy;yJyN_y?SwJ=kB?+B<8n{ms_ze{`k( zx^yX3y0p5g>UKjYj4YE<*mUm>6 zG)j{+%e(TPY?dvuRa&G~wn>}3FWY5@d>|jnNAj_JBA?1`{nLQcw$a!O9i8R?X>a!$@mmt2sG z^3xT%Q+>S`)7cY=-k;Oi!|@&)hz-OBVgs>(*g$L`HV_+#4a5fi7Xz7a;<(&f*4Le` z-@SUkztd@mj$q3GS+%etsJu2a%C+9i)jxiO87G3>XH z(R5sx=_|xxtQ_H$R1){sy)7i z%QS^uZUxFU+^+?~TH&D&U47^S`2qX_9$AaEc!IycnUO};ZRA*Sd8!`d(K+AV`}_8H zX3mlP?-yP;^Ur@hf966lKR^GE=wAY>nQOU9)e4>s1Olf56M?5*78n1S2%HX_4d|++ zp1yngCWuqS1o0Z;G;xM_fOswOI^y-jB=H8~jl`RXHxmyMXNk8EZzUcg9wy#Kyq$Ol z@d)uw;!)yV#Jh?25bq`4M+AxY6CvUp@d4t4#D|Cv6X%J?h>s8#h{uUA5g{Z(CKMt{ zOc62SA~8+O5OE?wB#Dm_DPopL6PJjO5gFn#F-K&H95GK^Ar^={@p0l4#3E53mWX9y zh4>^*NW2%C6{xJf)se3kebLE;(W>%=#R zZxY`kzD+z!e24fh@jc=>;`_u8h#wL^B7RK#gt$fgl=vC(bK)1oFNt3fzb1Y|{Fe9~ z@q6MA#Ph@-i9Zp4ei66N+h6f|mDT{ml+v!Scy`EWdRDLt9GygDxs=~QVY zm&>M8<$NJmDrX8S>m~8Fm@BQ9@=J5M^+GKo!?7ULaK)5|5{%F#L ze0msJKbHtgN$o^0=hn(;C;Ro~V*W~gDN|l9rWfn>rl|#2n)Q^_H`NoGUtU_zX3DvA zRE|!E3Lc4Mn?Zt!IOH~Cq*vAR*; zv1UUdN2&8_msV|L3mvT*B5t7(tzMOc2a?#xrR+gXt(scF&`nW!4~N|$@u5uqbp zsNkpEhE=P5dMzZZ+^#4`_+vW;^(NhhqI}RWq@scPPE#{-yGG4AH!KTX>tx>8X>PkV z>zZV28&v^;FhG$y<_hmp!j)_xS#FADg|yw{hTFu;>ZaS#j?WIrX~MSY_M)LSVK~Xn zhP>>$`8%+-kmU$1sZ}FAw_~i-&6*V~sLku*pp_C0vYlKGLwd4lF?G{8st%2V>;T zF|LE-7-0+h4;|{)ePxX(?QCfFw%O=j56AZ*Ut8X>#`eKEe|g~J`p|Nx_jd8P-0q-j z0Xh9k7R001`AV8~R-Jp6w%x;YIVvaRSW=NM$nj)5q2A!b&a{Zc_maxNAB+cgcdeph8k1jN?hBByG6F} z9(;g1Ti14+2T+?@7t|NyS}YceN5iU?mwudh?*GliuU7WIWkB>P;NyIqB^GDR(7uMvQlr_Rn@@5&{VmlFEz_&*_zfKfvtl!Tz$=-dTI?Z?>-AXv@2G z`EuT<%7x|SUDfTu%Cf3?i&iXMoC<;3RkBj5m38vA)W|#XuDmDj%LnqI zte1~ugM2KX$VT~8K9gFhlTET&w#Zi5Cfnt6`9gNcm-3Z-Ejy)NcFAsOkUg?j_KC7z z4#+_{B!}f2`BuJ@MmZu)(k$Q0Q8^|*$Z<)_3Heb@$|?CtPRkiNE9ay|ewJ2glk;*x zF8;=)yv~jb$;`<_$FIpuQ@qCpVgs>(*g$L`HV_+#4a5dw1F?ZeVjvw(99KF@J1-@x zcCGI9|8yGS;PDaNiIAm#x7SA8c<)Y5#C}1LXlqCWw>PcN4a4AWm`sK&4qE?g%U}1` znp0gHqI|_KWfLQ2*PU5Ctht9Z2c3zSN9L12WswT69xZ- zjHct_bZ1efcS%oya65M`dmcpRVO(9`ozrt~LV7__W_00$Mjk30Bswh0lqACM{Rc^4 BpF02m 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;