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 ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig>($"{k_audioConfigPath}/AmbienceTransition");
|
||||
|
||||
//运行时数据验证
|
||||
segments.Validate();
|
||||
containers.Validate(segments);
|
||||
|
||||
// MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上
|
||||
this.m_musicSystem = gameObject.AddComponent<MusicSystem>();
|
||||
|
||||
@@ -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<T>
|
||||
{
|
||||
public T[] AudioObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<AudioSource> sources = new();
|
||||
@@ -242,8 +249,8 @@ namespace OCES.Audio
|
||||
if (src) src.volume = 1f;
|
||||
|
||||
CurrentVolume = 1f;
|
||||
|
||||
// Debug.Log($"[ChannelFader] Faded in in {duration} seconds.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace OCES.Audio
|
||||
/// 开始播放Container时触发 音乐回调系统
|
||||
/// container本身,继承来的bpm,进入时刻的dspTime
|
||||
/// </summary>
|
||||
internal event Action<MusicContainer, float, double> OnContainerEntered;
|
||||
internal event Action<MusicContainer, float, double> OnContainerEntered, OnContainerLooped;
|
||||
internal event Action<MusicContainer> 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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user