Files
AudioSystem/Assets/Scripts/OCES/Audio/HandWritten/LongAudio/ChannelFader.cs
T
Oliver 8b6fabda12 feat: StartOffset
- 实现startOffset
- 修复EndOffset = 0 + 循环播放时,音乐会大量重复播放的错误。
- 增加数据校验和 BeatClock 联动。StartOffset不正确时停止bar+级 callback。
- BeatClock 现在会在每次重新播放时重启,以解决EndOffset配置错误被舍弃时,拍子对不上的问题。
2026-05-12 15:47:04 +08:00

257 lines
9.3 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
namespace OCES.Audio
{
/// <summary>
/// SyncPoint 协调状态,在 FadeOutBranch 和 FadeInBranch 之间共享。
/// </summary>
internal class SyncPointState
{
public SyncPoint Mode;
public int BaseTimeSamples; // 读取Source Segment 的 timeSamples
public double BaseDspTime; // 读取Source Segment 的 dspTime
public int SampleRate; // Source Segment 的采样率
public bool Ready;
public double SourceStartOffset;
public double TargetAudioTime;
public double StartPlayTime;
}
/// <summary>
/// 与Transition无关的音量/生命周期管理逻辑
/// </summary>
public class ChannelFader
{
readonly LongAudioContainerPlayer m_player;
readonly MonoBehaviour m_coroutineHost;
internal ContainerPlayHandle CurrentHandle { get; private set; }
public uint CurrentContainerId { get; private set; }
public float CurrentVolume { get; private set; }
internal ChannelFader(LongAudioContainerPlayer player, MonoBehaviour coroutineHost)
{
this.m_player = player;
this.m_coroutineHost = coroutineHost;
}
void StartNew(uint containerId, float startVolume)
{
CurrentContainerId = containerId;
CurrentVolume = startVolume;
CurrentHandle = this.m_player.Play(containerId);
//Debug.Log($"[ChannelFader] StartNew: containerId={containerId}, CurrentHandle={CurrentHandle}");
}
public void StopCurrent()
{
//Debug.Log($"[ChannelFader] StopCurrent called! CurrentHandle={CurrentHandle}, stack=\n{Environment.StackTrace}");
StopHandle(CurrentHandle);
CurrentHandle = null;
CurrentContainerId = 0;
}
void StopHandle(ContainerPlayHandle handle)
{
if (handle == null) return;
this.m_player.Stop(handle);
}
/// <summary>
/// 淡出分支:fire-and-forget,由调用方 StartCoroutine
/// </summary>
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));
}
else
{
StopHandle(outgoingHandle);
}
}
/// <summary>
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
/// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。
/// </summary>
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);
onContainerStarted?.Invoke();
switch (syncState)
{
// SyncPoint: 获取新 Segment 的 AudioSource 并设置 timeSamples
case { Mode: SyncPoint.SameAsCurrentSegment }:
{
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (newSource && newSource.clip)
{
double targetAudioTime = syncState.TargetAudioTime;
if (targetAudioTime > 0)
{
int targetTimeSamples = (int)(targetAudioTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
else
{
Debug.LogError(
$"[ChannelFader] SyncPoint: 目标 samples({targetTimeSamples}) 超出范围 [0, {newSource.clip.samples}),降级为 Start");
}
}
}
else
{
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 未能获取新 Segment 的 AudioSource,降级为 Start");
}
syncState.Ready = true;
break;
}
case { Mode: SyncPoint.Start, StartPlayTime: > 0 }:
{
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (!newSource || !newSource.clip)
yield break;
//在赋值的时候减过fade in time了
int targetTimeSamples = (int)(syncState.StartPlayTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
break;
}
}
if (transition?.FadeInTime > 0f)
{
yield return this.m_coroutineHost.StartCoroutine(
FadeIn(CurrentHandle, transition.FadeInTime));
}
}
IEnumerator FadeOut(ContainerPlayHandle handle, float fromVolume, float duration)
{
// Debug.Log($"Fading out in {duration} seconds.");
if (handle == null || handle.Cancelled) yield break;
float elapsed = 0f;
List<AudioSource> sources = new();
handle.CollectActiveSources(sources);
while (elapsed < duration)
{
if (handle.Cancelled) break;
elapsed += Time.deltaTime;
// 为每个 source 创建 DOFade(仅创建一次)
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
{
if (!src) continue;
if (DOTween.IsTweening(src))
continue;
src.volume = fromVolume;
src.DOFade(0f, duration - elapsed).SetEase(Ease.InSine); //TODO 支持读表
}
yield return null;
}
// 确保最终状态
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
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();
float elapsed = 0f;
while (elapsed < duration)
{
if (handle.Cancelled) yield break;
elapsed += Time.deltaTime;
// 每帧收集当前活跃的 sources,并为新加入的 source 创建 tween
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
{
if (!src) continue;
// 如果这个 source 还没被 tween 过,则创建一个 DOFade
if (DOTween.IsTweening(src))
continue;
src.volume = 0f;
src.DOFade(1f, duration - elapsed).SetEase(Ease.OutSine); //TODO 支持读表
}
yield return null;
}
// 确保最终音量
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
if (src) src.volume = 1f;
CurrentVolume = 1f;
// Debug.Log($"[ChannelFader] Faded in in {duration} seconds.");
}
}
}