feat: StartOffset
- 实现startOffset - 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误。 - 增加数据校验和 BeatClock 联动。StartOffset不正确时停止bar+级 callback。 - BeatClock 现在会在每次重新播放时重启,以解决EndOffset配置错误被舍弃时,拍子对不上的问题。
This commit is contained in:
@@ -302,6 +302,10 @@ namespace OCES.Audio
|
|||||||
var musicTransitions = AudioConfigLoader.Load<MusicTransitionConfig>($"{k_audioConfigPath}/MusicTransition");
|
var musicTransitions = AudioConfigLoader.Load<MusicTransitionConfig>($"{k_audioConfigPath}/MusicTransition");
|
||||||
var ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig>($"{k_audioConfigPath}/AmbienceTransition");
|
var ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig>($"{k_audioConfigPath}/AmbienceTransition");
|
||||||
|
|
||||||
|
//运行时数据验证
|
||||||
|
segments.Validate();
|
||||||
|
containers.Validate(segments);
|
||||||
|
|
||||||
// MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上
|
// MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上
|
||||||
this.m_musicSystem = gameObject.AddComponent<MusicSystem>();
|
this.m_musicSystem = gameObject.AddComponent<MusicSystem>();
|
||||||
|
|
||||||
@@ -338,6 +342,7 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
|
// Debug.Log("[AudioSystem] Start");
|
||||||
// ── 启动默认音乐与环境音 ──
|
// ── 启动默认音乐与环境音 ──
|
||||||
// 触发一次初始状态,让音乐系统从默认状态开始匹配
|
// 触发一次初始状态,让音乐系统从默认状态开始匹配
|
||||||
if (this.startWithMusic)
|
if (this.startWithMusic)
|
||||||
@@ -389,7 +394,7 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
T config = new T();
|
T config = new();
|
||||||
using MemoryStream ms = new(File.ReadAllBytes(path));
|
using MemoryStream ms = new(File.ReadAllBytes(path));
|
||||||
using BinaryReader reader = new(ms);
|
using BinaryReader reader = new(ms);
|
||||||
config.DeSerialize(reader);
|
config.DeSerialize(reader);
|
||||||
@@ -414,10 +419,5 @@ namespace OCES.Audio
|
|||||||
Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}");
|
Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}");
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioObjectArrayWrapper<T>
|
|
||||||
{
|
|
||||||
public T[] AudioObjects;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
internal void Restart(MusicContainer container, float inheritedBpm, double dspTime, double startOffset = 0d)
|
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();
|
StopAll();
|
||||||
this.m_blendError = this.m_stopped = false;
|
this.m_blendError = this.m_stopped = false;
|
||||||
this.m_containerId = container.Id;
|
this.m_containerId = container.Id;
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ namespace OCES.Audio
|
|||||||
CurrentContainerId = containerId;
|
CurrentContainerId = containerId;
|
||||||
CurrentVolume = startVolume;
|
CurrentVolume = startVolume;
|
||||||
CurrentHandle = this.m_player.Play(containerId);
|
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()
|
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);
|
StopHandle(CurrentHandle);
|
||||||
CurrentHandle = null;
|
CurrentHandle = null;
|
||||||
CurrentContainerId = 0;
|
CurrentContainerId = 0;
|
||||||
@@ -79,11 +79,15 @@ namespace OCES.Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (transition?.FadeOutTime > 0f )
|
if (transition?.FadeOutTime > 0f )
|
||||||
|
{
|
||||||
yield return this.m_coroutineHost.StartCoroutine(
|
yield return this.m_coroutineHost.StartCoroutine(
|
||||||
FadeOut(outgoingHandle, outgoingVolume, transition.FadeOutTime));
|
FadeOut(outgoingHandle, outgoingVolume, transition.FadeOutTime));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
StopHandle(outgoingHandle);
|
StopHandle(outgoingHandle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
|
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
|
||||||
@@ -202,10 +206,13 @@ namespace OCES.Audio
|
|||||||
if (src) src.volume = 0f;
|
if (src) src.volume = 0f;
|
||||||
|
|
||||||
StopHandle(handle);
|
StopHandle(handle);
|
||||||
|
|
||||||
|
// Debug.Log($"[ChannelFader] Faded out in {duration} seconds.");
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator FadeIn(ContainerPlayHandle handle, float duration)
|
IEnumerator FadeIn(ContainerPlayHandle handle, float duration)
|
||||||
{
|
{
|
||||||
|
// Debug.Log($"Fading in in {duration} seconds.");
|
||||||
if (handle == null || handle.Cancelled) yield break;
|
if (handle == null || handle.Cancelled) yield break;
|
||||||
|
|
||||||
List<AudioSource> sources = new();
|
List<AudioSource> sources = new();
|
||||||
@@ -242,8 +249,8 @@ namespace OCES.Audio
|
|||||||
if (src) src.volume = 1f;
|
if (src) src.volume = 1f;
|
||||||
|
|
||||||
CurrentVolume = 1f;
|
CurrentVolume = 1f;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
// Debug.Log($"[ChannelFader] Faded in in {duration} seconds.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace OCES.Audio
|
|||||||
/// 开始播放Container时触发 音乐回调系统
|
/// 开始播放Container时触发 音乐回调系统
|
||||||
/// container本身,继承来的bpm,进入时刻的dspTime
|
/// container本身,继承来的bpm,进入时刻的dspTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal event Action<MusicContainer, float, double> OnContainerEntered;
|
internal event Action<MusicContainer, float, double> OnContainerEntered, OnContainerLooped;
|
||||||
internal event Action<MusicContainer> OnBlendError;
|
internal event Action<MusicContainer> OnBlendError;
|
||||||
|
|
||||||
// Sequence Step 模式的全局游标,key = containerId
|
// Sequence Step 模式的全局游标,key = containerId
|
||||||
@@ -119,6 +119,7 @@ namespace OCES.Audio
|
|||||||
if (handle.Cancelled) yield break;
|
if (handle.Cancelled) yield break;
|
||||||
|
|
||||||
loopsCompleted++;
|
loopsCompleted++;
|
||||||
|
OnContainerLooped?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
|
||||||
|
|
||||||
// -1 = 无限循环,一直重复
|
// -1 = 无限循环,一直重复
|
||||||
if (container.LoopCount == -1)
|
if (container.LoopCount == -1)
|
||||||
@@ -352,6 +353,7 @@ namespace OCES.Audio
|
|||||||
source.loop = false;
|
source.loop = false;
|
||||||
source.volume = volumeScale;
|
source.volume = volumeScale;
|
||||||
source.Play();
|
source.Play();
|
||||||
|
Debug.Log($"[LongAudioContainerPlayer] Playing {segment.Name} at {AudioSettings.dspTime}");
|
||||||
|
|
||||||
if (isLoop && segment.StartOffset > 0)
|
if (isLoop && segment.StartOffset > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace OCES.Audio
|
|||||||
this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid);
|
this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid);
|
||||||
|
|
||||||
player.OnBlendError += this.m_beatClock.OnBlendError;
|
player.OnBlendError += this.m_beatClock.OnBlendError;
|
||||||
|
player.OnContainerLooped += OnContainerLooped;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@@ -58,12 +59,11 @@ namespace OCES.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SwitchTo(uint newContainerId)
|
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)
|
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null)
|
||||||
return; // 已经在播目标,无需切换
|
return; // 已经在播目标,无需切换
|
||||||
|
|
||||||
MusicTransition transition = ResolveTransition((int)this.m_fader.CurrentContainerId, (int)newContainerId);
|
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)
|
if (this.m_transitionCoroutine != null)
|
||||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||||
@@ -91,6 +91,7 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
IEnumerator DoTransition(uint newContainerId, MusicTransition transition)
|
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)
|
if (this.m_currentFadeInCoroutine != null)
|
||||||
{
|
{
|
||||||
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
|
this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine);
|
||||||
@@ -114,14 +115,15 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
if (syncState.Mode == SyncPoint.SameAsCurrentSegment)
|
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
|
// 旧 Container 不存在(如游戏首次启动),静默降级为 Start
|
||||||
if (this.m_fader.CurrentHandle == null)
|
if (this.m_fader.CurrentHandle == null)
|
||||||
{
|
{
|
||||||
syncState.Mode = SyncPoint.Start;
|
syncState.Mode = SyncPoint.Start;
|
||||||
}
|
}
|
||||||
// Blend 类型不支持 SameAsCurrentSegment,降级为 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");
|
Debug.LogWarning($"[MusicChannelPlayer] SyncPoint.SameAsCurrentSegment 不支持 Blend 类型 Container({this.m_currentContainer.Id}),降级为 Start");
|
||||||
syncState.Mode = SyncPoint.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)
|
if (transition != null && this.m_currentContainer != null)
|
||||||
{
|
{
|
||||||
|
Debug.Log("[MusicChannelPlayer] Waiting for Alignment.");
|
||||||
yield return this.m_coroutineHost.StartCoroutine(
|
yield return this.m_coroutineHost.StartCoroutine(
|
||||||
WaitForAlignment(transition.AlignMode, this.m_currentContainer));
|
WaitForAlignment(transition.AlignMode, this.m_currentContainer));
|
||||||
}
|
}
|
||||||
@@ -158,6 +161,7 @@ namespace OCES.Audio
|
|||||||
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
|
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
|
||||||
float outVol = this.m_fader.CurrentVolume;
|
float outVol = this.m_fader.CurrentVolume;
|
||||||
|
|
||||||
|
// Debug.Log("[MusicChannelPlayer] Start Crossfade.");
|
||||||
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
|
this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine(
|
||||||
this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState));
|
this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState));
|
||||||
|
|
||||||
@@ -200,6 +204,7 @@ namespace OCES.Audio
|
|||||||
double fadeInTime = transition?.FadeInTime ?? 0f;
|
double fadeInTime = transition?.FadeInTime ?? 0f;
|
||||||
startPlayTime = newStartOffset - fadeInTime;
|
startPlayTime = newStartOffset - fadeInTime;
|
||||||
startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity);
|
startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity);
|
||||||
|
newStartOffset = 0;
|
||||||
}
|
}
|
||||||
syncState.StartPlayTime = startPlayTime;
|
syncState.StartPlayTime = startPlayTime;
|
||||||
dspTime = AudioSettings.dspTime;
|
dspTime = AudioSettings.dspTime;
|
||||||
@@ -277,5 +282,12 @@ namespace OCES.Audio
|
|||||||
return child != null ? GetEffectiveStartOffset(child) : 0.0;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace OCES.Audio
|
namespace OCES.Audio
|
||||||
{
|
{
|
||||||
public partial class MusicContainerConfig
|
public partial class MusicContainerConfig
|
||||||
@@ -13,5 +15,26 @@ namespace OCES.Audio
|
|||||||
return beats;
|
return beats;
|
||||||
return 4;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<AudioClip>($"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+26
-26
@@ -3,8 +3,8 @@
|
|||||||
### 测试优先级
|
### 测试优先级
|
||||||
|
|
||||||
1. ~~**P0 冒烟**:#1, #2, #5, #25(核心路径 + 回归)~~
|
1. ~~**P0 冒烟**:#1, #2, #5, #25(核心路径 + 回归)~~
|
||||||
2. **P1 功能**:#9, #10, #13, #14, #16, #20(SyncPoint + BeatClock + Align)
|
2. ~~**P1 功能**:#9, #10, #13, #14, #16, #20(SyncPoint + BeatClock + Align)~~
|
||||||
3. **P2 边界**:#3, #4, #11, #18, #19, #23, #28, #29
|
3. ~~ **P2 边界**:#3, #4, #11, #18, #19, #23, #28, #29 ~~
|
||||||
4. **P3 回归**:#6, #7, #8, #12, #15, #17, #21, #22, #26, #27, #30, #31
|
4. **P3 回归**:#6, #7, #8, #12, #15, #17, #21, #22, #26, #27, #30, #31
|
||||||
|
|
||||||
### 一、StartOffset 核心功能
|
### 一、StartOffset 核心功能
|
||||||
@@ -13,72 +13,72 @@
|
|||||||
|---|---------|-------|:--------:|
|
|---|---------|-------|:--------:|
|
||||||
| 1 | 播放 StartOffset=5s 的 Segment | AudioClip 从 0 开始播放(可听到前奏);BeatClock 在 5s 后才触发第一次 Beat 回调 | ✅
|
| 1 | 播放 StartOffset=5s 的 Segment | AudioClip 从 0 开始播放(可听到前奏);BeatClock 在 5s 后才触发第一次 Beat 回调 | ✅
|
||||||
| 2 | 播放 StartOffset=0 的 Segment | 行为与改动前完全一致 | ✅
|
| 2 | 播放 StartOffset=0 的 Segment | 行为与改动前完全一致 | ✅
|
||||||
| 3 | StartOffset + EndOffset 组合 | 播放到 `clip.length - EndOffset` 时 Segment 逻辑结束,物理播放继续到自然结束 |
|
| 3 | StartOffset + EndOffset 组合 | 播放到 `clip.length - EndOffset` 时 Segment 逻辑结束,物理播放继续到自然结束 | ✅
|
||||||
| 4 | StartOffset 值大于 Clip 长度 | 应有合理降级(当前为 TODO,至少不崩溃) |
|
| 4 | StartOffset 值大于 Clip 长度 | 应有合理降级(当前为 TODO,至少不崩溃) | ✅
|
||||||
|
|
||||||
### 二、循环播放
|
### 二、循环播放
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 5 | LoopCount=-1,StartOffset=5s | 第一轮从 0 播放;第二轮及之后从 5s 开始播放 | ✅
|
| 5 | LoopCount=-1,StartOffset=5s | 第一轮从 0 播放;第二轮及之后从 5s 开始播放 | ✅
|
||||||
| 6 | LoopCount=2,StartOffset=5s | 第一轮从 0 播放,第二轮从 5s 开始,播满 2 轮后停止 |
|
| 6 | LoopCount=2,StartOffset=5s | 第一轮从 0 播放,第二轮从 5s 开始,播满 2 轮后停止 | ✅
|
||||||
| 7 | LoopCount=-1,StartOffset=0 | 行为与改动前完全一致 |
|
| 7 | LoopCount=-1,StartOffset=0 | 行为与改动前完全一致 | ✅
|
||||||
| 8 | 循环时 BeatClock | 每次循环 BeatClock 重新启动,逻辑时间从 0 重新开始计数 |
|
| 8 | 循环时 BeatClock | 每次循环 BeatClock 重新启动,逻辑时间从 0 重新开始计数 | ✅
|
||||||
|
|
||||||
### 三、SyncPoint.SameAsCurrentSegment
|
### 三、SyncPoint.SameAsCurrentSegment
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 9 | 旧 Container StartOffset=5s,播放到 15s 时切换;新 Container StartOffset=3s | 逻辑时长=15-5=10s;新位置=10+3=13s;新 AudioSource.timeSamples 对应 13s | ✅ |
|
| 9 | 旧 Segment StartOffset=5s,播放到 15s 时切换;新 Segment StartOffset=3s | 逻辑时长=15-5=10s;新位置=10+3=13s;新 AudioSource.timeSamples 对应 13s | ✅ |
|
||||||
| 10 | 旧 Container StartOffset=5s,播放到 3s 时切换(还没过 StartOffset) | 新 Container 从自身 StartOffset 开始播放 | ✅ |
|
| 10 | 旧 Segment StartOffset=5s,播放到 3s 时切换(还没过 StartOffset) | 新 Segment 从自身 StartOffset 开始播放 | ✅ |
|
||||||
| 11 | 计算出新位置 < 新 StartOffset | 新 Container 从自身 StartOffset 开始播放 |
|
| 11 | 计算出新位置 < 新 StartOffset | 新 Segment 从自身 StartOffset 开始播放 | ✅
|
||||||
| 12 | 旧 Container 无 StartOffset,新 Container 有 StartOffset | 旧 SourceStartOffset=0,计算逻辑正确 |
|
| 12 | 旧 Segment 无 StartOffset,新 Segment 有 StartOffset | 旧 SourceStartOffset=0,计算逻辑正确 |
|
||||||
|
|
||||||
### 四、SyncPoint.Start
|
### 四、SyncPoint.Start
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 13 | StartOffset=5s,FadeInTime=2s | AudioSource 从 3s 开始播放(5-2=3),淡入 2s 后刚好到 5s |
|
| 13 | StartOffset=5s,FadeInTime=2s | AudioSource 从 3s 开始播放(5-2=3),淡入 2s 后刚好到 5s | ✅
|
||||||
| 14 | StartOffset=1s,FadeInTime=3s | StartPlayTime=1-3=-2→0,从头开始淡入,忽略 StartOffset |
|
| 14 | StartOffset=1s,FadeInTime=3s | StartPlayTime=1-3=-2→0,从头开始淡入,忽略 StartOffset | ✅
|
||||||
| 15 | StartOffset=5s,FadeInTime=0 | StartPlayTime=5,从 5s 开始播放(无淡入) |
|
| 15 | StartOffset=5s,FadeInTime=0 | StartPlayTime=5,从 5s 开始播放(无淡入) | ✅
|
||||||
|
|
||||||
### 五、BeatClock 延迟启动
|
### 五、BeatClock 延迟启动
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 16 | 首次启动 StartOffset=5s | BeatClock 等 5s 后才触发首次 Beat 回调 |
|
| 16 | 首次启动 StartOffset=5s | BeatClock 等 5s 后才触发首次 Beat 回调 | ✅
|
||||||
| 17 | 首次启动 StartOffset=0 | BeatClock 立即启动,行为不变 |
|
| 17 | 首次启动 StartOffset=0 | BeatClock 立即启动,行为不变 |
|
||||||
| 18 | 延迟期间调用 Stop() | BeatClock 立即停止,不触发任何回调 |
|
| 18 | 延迟期间调用 Stop() | BeatClock 立即停止,不触发任何回调 | ✅
|
||||||
| 19 | SameAsCurrentSegment 切换时逻辑起点已过 | BeatClock 立即启动(delay<=0),从正确的逻辑时间继续 |
|
| 19 | SameAsCurrentSegment 切换时逻辑起点已过 | BeatClock 立即启动(delay<=0),从正确的逻辑时间继续 | 不会有 这个情况
|
||||||
|
|
||||||
### 六、对齐(AlignMode)
|
### 六、对齐(AlignMode)
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 20 | AlignMode.Beat,StartOffset=5s | 对齐基于逻辑起点(m_startDspTime = dspTime + 5),不是物理起点 |
|
| 20 | AlignMode.Beat,StartOffset=5s | 对齐基于逻辑起点(m_startDspTime = dspTime + 5),不是物理起点 | ✅
|
||||||
| 21 | AlignMode.Bar,StartOffset=5s | 同上 |
|
| 21 | AlignMode.Bar,StartOffset=5s | 同上 | ✅
|
||||||
| 22 | 在 StartOffset 延迟期间调用 GetNextDspTime | 返回 m_startDspTime(逻辑起点),不会返回过去的时间 |
|
| 22 | 在 StartOffset 延迟期间调用 GetNextDspTime | 返回 m_startDspTime(逻辑起点),不会返回过去的时间 | 要怎么测试?
|
||||||
|
|
||||||
### 七、Blend Container
|
### 七、Blend Container
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 23 | Blend Container 的子 Segment 有 StartOffset | GetEffectiveStartOffset 返回 0,isLoop=false,行为与改动前一致 |
|
| 23 | Blend Container 的子 Segment 有 StartOffset | GetEffectiveStartOffset 返回 0,isLoop=false,行为与改动前一致 | ✅
|
||||||
| 24 | SyncPoint.SameAsCurrentSegment 降级 | Blend 不支持 SameAsCurrentSegment,降级为 Start(原有行为不变) |
|
| 24 | SyncPoint.SameAsCurrentSegment 降级 | Blend 不支持 SameAsCurrentSegment,降级为 Start(原有行为不变) | ✅
|
||||||
|
|
||||||
### 八、回归测试(无 StartOffset 时行为不变)
|
### 八、回归测试(无 StartOffset 时行为不变)
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过
|
| # | 测试场景 | 验证点 | 是否通过
|
||||||
|---|---------|--------|:---:|
|
|---|---------|--------|:---:|
|
||||||
| 25 | 所有 Segment 的 StartOffset=0,EndOffset=0 | 全流程与改动前一致:播放、循环、切换、SyncPoint、FadeIn/Out | ✅
|
| 25 | 所有 Segment 的 StartOffset=0,EndOffset=0 | 全流程与改动前一致:播放、循环、切换、SyncPoint、FadeIn/Out | ✅
|
||||||
| 26 | Segment 只有 EndOffset,无 StartOffset | WaitSegmentFinish 到达 effectiveTime 后通知 container 推进 |
|
| 26 | Segment 只有 EndOffset,无 StartOffset | WaitSegmentFinish 到达 effectiveTime 后通知 container 推进 | ✅
|
||||||
| 27 | AmbienceChannelPlayer | 不涉及 BeatClock,StartOffset 仅影响 PlaySegment 的 isLoop 跳转,需确认无副作用 |
|
| 27 | AmbienceChannelPlayer | 不涉及 BeatClock,StartOffset 仅影响 PlaySegment 的 isLoop 跳转,需确认无副作用 | ✅
|
||||||
|
|
||||||
### 九、Sequence/Random Container 内多个 Segment
|
### 九、Sequence/Random Container 内多个 Segment
|
||||||
|
|
||||||
| # | 测试场景 | 验证点 | 是否通过 |
|
| # | 测试场景 | 验证点 | 是否通过 |
|
||||||
|---|---------|--------|:--------:|
|
|---|---------|--------|:--------:|
|
||||||
| 28 | Sequence Container,3 个 Segment 各有不同 StartOffset,首次播放 | 所有 Segment 从 0 开始播放 |
|
| 28 | Sequence Container,3 个 Segment 各有不同 StartOffset,首次播放 | 所有 Segment 从 0 开始播放 | ✅
|
||||||
| 29 | 同上,LoopCount=-1 | 第二轮起所有 Segment 从各自 StartOffset 开始 |
|
| 29 | 同上,LoopCount=-1 | 第二轮起所有 Segment 从各自 StartOffset 开始 | ✅
|
||||||
| 30 | Random Container,循环时 | 选中 Segment 从其 StartOffset 开始 |
|
| 30 | Random Container,循环时 | 选中 Segment 从其 StartOffset 开始 |
|
||||||
| 31 | Step Sequence Container | 每次切 Segment 时 isLoop 传递正确 |
|
| 31 | Step Sequence Container | 每次切 Segment 时 isLoop 传递正确 |
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
feat: StartOffset
|
|
||||||
|
|
||||||
- 实现startOffset
|
|
||||||
- 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误
|
|
||||||
Reference in New Issue
Block a user