From 8b6fabda12e052d94cf043c6e0dc4fe1afa8d5e7 Mon Sep 17 00:00:00 2001 From: Oliver Wong Date: Tue, 12 May 2026 15:47:04 +0800 Subject: [PATCH] feat: StartOffset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现startOffset - 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误。 - 增加数据校验和 BeatClock 联动。StartOffset不正确时停止bar+级 callback。 - BeatClock 现在会在每次重新播放时重启,以解决EndOffset配置错误被舍弃时,拍子对不上的问题。 --- .../OCES/Audio/HandWritten/AudioSystem.cs | 12 ++-- .../Audio/HandWritten/LongAudio/BeatClock.cs | 2 +- .../HandWritten/LongAudio/ChannelFader.cs | 17 ++++-- .../LongAudio/LongAudioContainerPlayer.cs | 4 +- .../LongAudio/MusicChannelPlayer.cs | 22 ++++++-- .../HandWritten/LongAudio/MusicContainer.cs | 23 ++++++++ .../HandWritten/LongAudio/MusicSegment.cs | 53 ++++++++++++++++++ .../LongAudio/MusicSegmentConfig.cs | 10 ---- StartOffset 测试清单.md | 56 +++++++++---------- commit message draft.md | 4 -- 10 files changed, 143 insertions(+), 60 deletions(-) create mode 100644 Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegment.cs delete mode 100644 Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegmentConfig.cs delete mode 100644 commit message draft.md diff --git a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs index f6cfc47..940966b 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/AudioSystem.cs @@ -302,6 +302,10 @@ namespace OCES.Audio var musicTransitions = AudioConfigLoader.Load($"{k_audioConfigPath}/MusicTransition"); var ambienceTransitions = AudioConfigLoader.Load($"{k_audioConfigPath}/AmbienceTransition"); + //运行时数据验证 + segments.Validate(); + containers.Validate(segments); + // MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上 this.m_musicSystem = gameObject.AddComponent(); @@ -338,6 +342,7 @@ namespace OCES.Audio void Start() { + // Debug.Log("[AudioSystem] Start"); // ── 启动默认音乐与环境音 ── // 触发一次初始状态,让音乐系统从默认状态开始匹配 if (this.startWithMusic) @@ -389,7 +394,7 @@ namespace OCES.Audio try { - T config = new T(); + T config = new(); using MemoryStream ms = new(File.ReadAllBytes(path)); using BinaryReader reader = new(ms); config.DeSerialize(reader); @@ -414,10 +419,5 @@ namespace OCES.Audio Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}"); return default; } - - class AudioObjectArrayWrapper - { - public T[] AudioObjects; - } } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs index 9994cb7..ccc2f35 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/BeatClock.cs @@ -29,7 +29,7 @@ namespace OCES.Audio internal void Restart(MusicContainer container, float inheritedBpm, double dspTime, double startOffset = 0d) { - //Debug.Log($"[BeatClock] Restarting {container.Id}, inheritedBpm = {inheritedBpm}, dspTime = {dspTime}"); + Debug.Log($"[BeatClock] Restarting {container.Id}, inheritedBpm = {inheritedBpm}, dspTime = {dspTime}"); StopAll(); this.m_blendError = this.m_stopped = false; this.m_containerId = container.Id; diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs index 53a9295..12c7417 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs @@ -44,12 +44,12 @@ namespace OCES.Audio CurrentContainerId = containerId; CurrentVolume = startVolume; CurrentHandle = this.m_player.Play(containerId); - Debug.Log($"[ChannelFader] StartNew: containerId={containerId}, CurrentHandle={CurrentHandle}"); + //Debug.Log($"[ChannelFader] StartNew: containerId={containerId}, CurrentHandle={CurrentHandle}"); } public void StopCurrent() { - Debug.Log($"[ChannelFader] StopCurrent called! CurrentHandle={CurrentHandle}, stack=\n{Environment.StackTrace}"); + //Debug.Log($"[ChannelFader] StopCurrent called! CurrentHandle={CurrentHandle}, stack=\n{Environment.StackTrace}"); StopHandle(CurrentHandle); CurrentHandle = null; CurrentContainerId = 0; @@ -79,10 +79,14 @@ namespace OCES.Audio } if (transition?.FadeOutTime > 0f ) + { yield return this.m_coroutineHost.StartCoroutine( FadeOut(outgoingHandle, outgoingVolume, transition.FadeOutTime)); + } else + { StopHandle(outgoingHandle); + } } /// @@ -167,7 +171,7 @@ namespace OCES.Audio IEnumerator FadeOut(ContainerPlayHandle handle, float fromVolume, float duration) { - //Debug.Log($"Fading out in {duration} seconds."); + // Debug.Log($"Fading out in {duration} seconds."); if (handle == null || handle.Cancelled) yield break; float elapsed = 0f; @@ -202,10 +206,13 @@ namespace OCES.Audio if (src) src.volume = 0f; StopHandle(handle); + + // Debug.Log($"[ChannelFader] Faded out in {duration} seconds."); } IEnumerator FadeIn(ContainerPlayHandle handle, float duration) { + // Debug.Log($"Fading in in {duration} seconds."); if (handle == null || handle.Cancelled) yield break; List sources = new(); @@ -242,8 +249,8 @@ namespace OCES.Audio if (src) src.volume = 1f; CurrentVolume = 1f; + + // Debug.Log($"[ChannelFader] Faded in in {duration} seconds."); } - - } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/LongAudioContainerPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/LongAudioContainerPlayer.cs index 9116c01..2c9c48e 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/LongAudioContainerPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/LongAudioContainerPlayer.cs @@ -24,7 +24,7 @@ namespace OCES.Audio /// 开始播放Container时触发 音乐回调系统 /// container本身,继承来的bpm,进入时刻的dspTime /// - internal event Action OnContainerEntered; + internal event Action OnContainerEntered, OnContainerLooped; internal event Action OnBlendError; // Sequence Step 模式的全局游标,key = containerId @@ -119,6 +119,7 @@ namespace OCES.Audio if (handle.Cancelled) yield break; loopsCompleted++; + OnContainerLooped?.Invoke(container, effectiveBpm, AudioSettings.dspTime); // -1 = 无限循环,一直重复 if (container.LoopCount == -1) @@ -352,6 +353,7 @@ namespace OCES.Audio source.loop = false; source.volume = volumeScale; source.Play(); + Debug.Log($"[LongAudioContainerPlayer] Playing {segment.Name} at {AudioSettings.dspTime}"); if (isLoop && segment.StartOffset > 0) { diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs index 805b2c7..f4068f0 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicChannelPlayer.cs @@ -47,6 +47,7 @@ namespace OCES.Audio this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid); player.OnBlendError += this.m_beatClock.OnBlendError; + player.OnContainerLooped += OnContainerLooped; } // ───────────────────────────────────────────── @@ -58,12 +59,11 @@ namespace OCES.Audio /// internal void SwitchTo(uint newContainerId) { - Debug.Log($"[MusicChannelPlayer] SwitchTo({newContainerId}): CurrentContainerId={this.m_fader.CurrentContainerId}, CurrentHandle={this.m_fader.CurrentHandle}"); + //Debug.Log($"[MusicChannelPlayer] SwitchTo({newContainerId}): CurrentContainerId={this.m_fader.CurrentContainerId}, CurrentHandle={this.m_fader.CurrentHandle}"); if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) return; // 已经在播目标,无需切换 MusicTransition transition = ResolveTransition((int)this.m_fader.CurrentContainerId, (int)newContainerId); - //Debug.Log($"[MusicChannelPlayer] Switch from {this.m_fader.CurrentContainerId} to {newContainerId} with transition {transition.Id}"); if (this.m_transitionCoroutine != null) this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine); @@ -91,6 +91,7 @@ namespace OCES.Audio IEnumerator DoTransition(uint newContainerId, MusicTransition transition) { + // Debug.Log($"[MusicChannelPlayer] Scheduled transition from {this.m_fader.CurrentContainerId} to {newContainerId} with transition {transition.Id}"); if (this.m_currentFadeInCoroutine != null) { this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine); @@ -114,14 +115,15 @@ namespace OCES.Audio if (syncState.Mode == SyncPoint.SameAsCurrentSegment) { - Debug.Log($"[MusicChannelPlayer] DoTransition L117: CurrentHandle={this.m_fader.CurrentHandle}, CurrentContainerId={this.m_fader.CurrentContainerId}"); + // Debug.Log($"[MusicChannelPlayer] DoTransition L117: CurrentHandle={this.m_fader.CurrentHandle}, CurrentContainerId={this.m_fader.CurrentContainerId}"); // 旧 Container 不存在(如游戏首次启动),静默降级为 Start if (this.m_fader.CurrentHandle == null) { syncState.Mode = SyncPoint.Start; } // Blend 类型不支持 SameAsCurrentSegment,降级为 Start - else if (this.m_currentContainer is { ContainerType: ContainerType.Blend }) + else if (this.m_currentContainer is { ContainerType: ContainerType.Blend } + || this.m_containerConfig.QueryById(newContainerId) is { ContainerType: ContainerType.Blend }) { Debug.LogWarning($"[MusicChannelPlayer] SyncPoint.SameAsCurrentSegment 不支持 Blend 类型 Container({this.m_currentContainer.Id}),降级为 Start"); syncState.Mode = SyncPoint.Start; @@ -144,9 +146,10 @@ namespace OCES.Audio } } - // ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)── + // ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)── TODO 支持FadeInTime为负值时,等待FadeIn if (transition != null && this.m_currentContainer != null) { + Debug.Log("[MusicChannelPlayer] Waiting for Alignment."); yield return this.m_coroutineHost.StartCoroutine( WaitForAlignment(transition.AlignMode, this.m_currentContainer)); } @@ -158,6 +161,7 @@ namespace OCES.Audio ContainerPlayHandle outgoing = this.m_fader.CurrentHandle; float outVol = this.m_fader.CurrentVolume; + // Debug.Log("[MusicChannelPlayer] Start Crossfade."); this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine( this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState)); @@ -200,6 +204,7 @@ namespace OCES.Audio double fadeInTime = transition?.FadeInTime ?? 0f; startPlayTime = newStartOffset - fadeInTime; startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity); + newStartOffset = 0; } syncState.StartPlayTime = startPlayTime; dspTime = AudioSettings.dspTime; @@ -277,5 +282,12 @@ namespace OCES.Audio return child != null ? GetEffectiveStartOffset(child) : 0.0; } + + void OnContainerLooped(MusicContainer container, float bpm, double dspTime) + { + if (this.m_currentContainer == null || this.m_currentContainer.Id != container.Id) + return; + this.m_beatClock.Restart(container, bpm, dspTime); + } } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainer.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainer.cs index 741e5ec..bc7e336 100644 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainer.cs +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicContainer.cs @@ -1,3 +1,5 @@ +using UnityEngine; + namespace OCES.Audio { public partial class MusicContainerConfig @@ -13,5 +15,26 @@ namespace OCES.Audio return beats; return 4; } + + public void Validate(MusicSegmentConfig segmentConfig) + { + foreach (MusicContainer container in this.m_musicContainerInfos.Values) + { + foreach (uint segmentId in container.Segments) + { + if (segmentId < 1000000) + { + MusicSegment segment = segmentConfig.QueryById(segmentId); + if (segment is { IsOffBeat: true }) + { + container.TimeSig = ""; + Debug.LogWarning($"[AudioSystem] {container.Id} Container含有错误配置的Segment {segment.Id}。" + + "切换至此Container时将不会启动beat/grid回调"); + break; + } + } + } + } + } } } diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegment.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegment.cs new file mode 100644 index 0000000..e61ac0d --- /dev/null +++ b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegment.cs @@ -0,0 +1,53 @@ +using UnityEngine; + +namespace OCES.Audio +{ + public partial class MusicSegment + { + public bool IsOffBeat { get; set; } + } + + + public partial class MusicSegmentConfig + { + // TODO: 负偏移功能待开发 + + public void Validate() + { + foreach (MusicSegment segment in this.m_musicSegmentInfos.Values) + { + AudioClip clip = Resources.Load($"Audios/{segment.Name}"); + if (!clip) + { + Debug.LogError($"[MusicSegmentConfig] 音频文件未找到: {segment.Name}, SegmentId: {segment.Id}"); + segment.StartOffset = 0; + segment.EndOffset = 0; + continue; + } + + double clipLength = clip.length; + + if (segment.StartOffset > clipLength) + { + Debug.LogError($"[MusicSegmentConfig] StartOffset({segment.StartOffset}) > AudioClip.length({clipLength}), SegmentId: {segment.Id}, Name: {segment.Name}" + + "已弃用 StartOffset"); + segment.StartOffset = 0; + segment.IsOffBeat = true; + } + + if (segment.EndOffset > clipLength) + { + Debug.LogError($"[MusicSegmentConfig] EndOffset({segment.EndOffset}) > AudioClip.length({clipLength}), SegmentId: {segment.Id}, Name: {segment.Name}"); + segment.EndOffset = 0; + } + + if (segment.StartOffset + segment.EndOffset > clipLength) + { + Debug.LogError($"[MusicSegmentConfig] StartOffset({segment.StartOffset}) + EndOffset({segment.EndOffset}) > AudioClip.length({clipLength}), SegmentId: {segment.Id}, Name: {segment.Name}"); + segment.StartOffset = segment.EndOffset = 0; + segment.IsOffBeat = true; + } + } + } + } +} diff --git a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegmentConfig.cs b/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegmentConfig.cs deleted file mode 100644 index 1fd7132..0000000 --- a/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/MusicSegmentConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OCES.Audio -{ - public partial class MusicSegmentConfig - { - // TODO: 运行前边界验证 - // - MusicSegment.StartOffset <= AudioClip.length - // - MusicSegment.EndOffset <= AudioClip.length - // - MusicSegment.StartOffset + MusicSegment.EndOffset <= AudioClip.length - } -} diff --git a/StartOffset 测试清单.md b/StartOffset 测试清单.md index 95e22a4..16ecde9 100644 --- a/StartOffset 测试清单.md +++ b/StartOffset 测试清单.md @@ -3,8 +3,8 @@ ### 测试优先级 1. ~~**P0 冒烟**:#1, #2, #5, #25(核心路径 + 回归)~~ -2. **P1 功能**:#9, #10, #13, #14, #16, #20(SyncPoint + BeatClock + Align) -3. **P2 边界**:#3, #4, #11, #18, #19, #23, #28, #29 +2. ~~**P1 功能**:#9, #10, #13, #14, #16, #20(SyncPoint + BeatClock + Align)~~ +3. ~~ **P2 边界**:#3, #4, #11, #18, #19, #23, #28, #29 ~~ 4. **P3 回归**:#6, #7, #8, #12, #15, #17, #21, #22, #26, #27, #30, #31 ### 一、StartOffset 核心功能 @@ -13,72 +13,72 @@ |---|---------|-------|:--------:| | 1 | 播放 StartOffset=5s 的 Segment | AudioClip 从 0 开始播放(可听到前奏);BeatClock 在 5s 后才触发第一次 Beat 回调 | ✅ | 2 | 播放 StartOffset=0 的 Segment | 行为与改动前完全一致 | ✅ -| 3 | StartOffset + EndOffset 组合 | 播放到 `clip.length - EndOffset` 时 Segment 逻辑结束,物理播放继续到自然结束 | -| 4 | StartOffset 值大于 Clip 长度 | 应有合理降级(当前为 TODO,至少不崩溃) | +| 3 | StartOffset + EndOffset 组合 | 播放到 `clip.length - EndOffset` 时 Segment 逻辑结束,物理播放继续到自然结束 | ✅ +| 4 | StartOffset 值大于 Clip 长度 | 应有合理降级(当前为 TODO,至少不崩溃) | ✅ ### 二、循环播放 | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| | 5 | LoopCount=-1,StartOffset=5s | 第一轮从 0 播放;第二轮及之后从 5s 开始播放 | ✅ -| 6 | LoopCount=2,StartOffset=5s | 第一轮从 0 播放,第二轮从 5s 开始,播满 2 轮后停止 | -| 7 | LoopCount=-1,StartOffset=0 | 行为与改动前完全一致 | -| 8 | 循环时 BeatClock | 每次循环 BeatClock 重新启动,逻辑时间从 0 重新开始计数 | +| 6 | LoopCount=2,StartOffset=5s | 第一轮从 0 播放,第二轮从 5s 开始,播满 2 轮后停止 | ✅ +| 7 | LoopCount=-1,StartOffset=0 | 行为与改动前完全一致 | ✅ +| 8 | 循环时 BeatClock | 每次循环 BeatClock 重新启动,逻辑时间从 0 重新开始计数 | ✅ ### 三、SyncPoint.SameAsCurrentSegment | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 9 | 旧 Container StartOffset=5s,播放到 15s 时切换;新 Container StartOffset=3s | 逻辑时长=15-5=10s;新位置=10+3=13s;新 AudioSource.timeSamples 对应 13s | ✅ | -| 10 | 旧 Container StartOffset=5s,播放到 3s 时切换(还没过 StartOffset) | 新 Container 从自身 StartOffset 开始播放 | ✅ | -| 11 | 计算出新位置 < 新 StartOffset | 新 Container 从自身 StartOffset 开始播放 | -| 12 | 旧 Container 无 StartOffset,新 Container 有 StartOffset | 旧 SourceStartOffset=0,计算逻辑正确 | +| 9 | 旧 Segment StartOffset=5s,播放到 15s 时切换;新 Segment StartOffset=3s | 逻辑时长=15-5=10s;新位置=10+3=13s;新 AudioSource.timeSamples 对应 13s | ✅ | +| 10 | 旧 Segment StartOffset=5s,播放到 3s 时切换(还没过 StartOffset) | 新 Segment 从自身 StartOffset 开始播放 | ✅ | +| 11 | 计算出新位置 < 新 StartOffset | 新 Segment 从自身 StartOffset 开始播放 | ✅ +| 12 | 旧 Segment 无 StartOffset,新 Segment 有 StartOffset | 旧 SourceStartOffset=0,计算逻辑正确 | ### 四、SyncPoint.Start | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 13 | StartOffset=5s,FadeInTime=2s | AudioSource 从 3s 开始播放(5-2=3),淡入 2s 后刚好到 5s | -| 14 | StartOffset=1s,FadeInTime=3s | StartPlayTime=1-3=-2→0,从头开始淡入,忽略 StartOffset | -| 15 | StartOffset=5s,FadeInTime=0 | StartPlayTime=5,从 5s 开始播放(无淡入) | +| 13 | StartOffset=5s,FadeInTime=2s | AudioSource 从 3s 开始播放(5-2=3),淡入 2s 后刚好到 5s | ✅ +| 14 | StartOffset=1s,FadeInTime=3s | StartPlayTime=1-3=-2→0,从头开始淡入,忽略 StartOffset | ✅ +| 15 | StartOffset=5s,FadeInTime=0 | StartPlayTime=5,从 5s 开始播放(无淡入) | ✅ ### 五、BeatClock 延迟启动 | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 16 | 首次启动 StartOffset=5s | BeatClock 等 5s 后才触发首次 Beat 回调 | +| 16 | 首次启动 StartOffset=5s | BeatClock 等 5s 后才触发首次 Beat 回调 | ✅ | 17 | 首次启动 StartOffset=0 | BeatClock 立即启动,行为不变 | -| 18 | 延迟期间调用 Stop() | BeatClock 立即停止,不触发任何回调 | -| 19 | SameAsCurrentSegment 切换时逻辑起点已过 | BeatClock 立即启动(delay<=0),从正确的逻辑时间继续 | +| 18 | 延迟期间调用 Stop() | BeatClock 立即停止,不触发任何回调 | ✅ +| 19 | SameAsCurrentSegment 切换时逻辑起点已过 | BeatClock 立即启动(delay<=0),从正确的逻辑时间继续 | 不会有 这个情况 ### 六、对齐(AlignMode) | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 20 | AlignMode.Beat,StartOffset=5s | 对齐基于逻辑起点(m_startDspTime = dspTime + 5),不是物理起点 | -| 21 | AlignMode.Bar,StartOffset=5s | 同上 | -| 22 | 在 StartOffset 延迟期间调用 GetNextDspTime | 返回 m_startDspTime(逻辑起点),不会返回过去的时间 | +| 20 | AlignMode.Beat,StartOffset=5s | 对齐基于逻辑起点(m_startDspTime = dspTime + 5),不是物理起点 | ✅ +| 21 | AlignMode.Bar,StartOffset=5s | 同上 | ✅ +| 22 | 在 StartOffset 延迟期间调用 GetNextDspTime | 返回 m_startDspTime(逻辑起点),不会返回过去的时间 | 要怎么测试? ### 七、Blend Container | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 23 | Blend Container 的子 Segment 有 StartOffset | GetEffectiveStartOffset 返回 0,isLoop=false,行为与改动前一致 | -| 24 | SyncPoint.SameAsCurrentSegment 降级 | Blend 不支持 SameAsCurrentSegment,降级为 Start(原有行为不变) | +| 23 | Blend Container 的子 Segment 有 StartOffset | GetEffectiveStartOffset 返回 0,isLoop=false,行为与改动前一致 | ✅ +| 24 | SyncPoint.SameAsCurrentSegment 降级 | Blend 不支持 SameAsCurrentSegment,降级为 Start(原有行为不变) | ✅ ### 八、回归测试(无 StartOffset 时行为不变) | # | 测试场景 | 验证点 | 是否通过 |---|---------|--------|:---:| | 25 | 所有 Segment 的 StartOffset=0,EndOffset=0 | 全流程与改动前一致:播放、循环、切换、SyncPoint、FadeIn/Out | ✅ -| 26 | Segment 只有 EndOffset,无 StartOffset | WaitSegmentFinish 到达 effectiveTime 后通知 container 推进 | -| 27 | AmbienceChannelPlayer | 不涉及 BeatClock,StartOffset 仅影响 PlaySegment 的 isLoop 跳转,需确认无副作用 | +| 26 | Segment 只有 EndOffset,无 StartOffset | WaitSegmentFinish 到达 effectiveTime 后通知 container 推进 | ✅ +| 27 | AmbienceChannelPlayer | 不涉及 BeatClock,StartOffset 仅影响 PlaySegment 的 isLoop 跳转,需确认无副作用 | ✅ ### 九、Sequence/Random Container 内多个 Segment | # | 测试场景 | 验证点 | 是否通过 | |---|---------|--------|:--------:| -| 28 | Sequence Container,3 个 Segment 各有不同 StartOffset,首次播放 | 所有 Segment 从 0 开始播放 | -| 29 | 同上,LoopCount=-1 | 第二轮起所有 Segment 从各自 StartOffset 开始 | -| 30 | Random Container,循环时 | 选中 Segment 从其 StartOffset 开始 | -| 31 | Step Sequence Container | 每次切 Segment 时 isLoop 传递正确 | \ No newline at end of file +| 28 | Sequence Container,3 个 Segment 各有不同 StartOffset,首次播放 | 所有 Segment 从 0 开始播放 | ✅ +| 29 | 同上,LoopCount=-1 | 第二轮起所有 Segment 从各自 StartOffset 开始 | ✅ +| 30 | Random Container,循环时 | 选中 Segment 从其 StartOffset 开始 | +| 31 | Step Sequence Container | 每次切 Segment 时 isLoop 传递正确 | \ No newline at end of file diff --git a/commit message draft.md b/commit message draft.md deleted file mode 100644 index 93fcc8c..0000000 --- a/commit message draft.md +++ /dev/null @@ -1,4 +0,0 @@ -feat: StartOffset - -- 实现startOffset -- 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误 \ No newline at end of file