first commit

This commit is contained in:
2026-03-20 17:55:53 +08:00
commit c5778c498b
328 changed files with 11867 additions and 0 deletions
@@ -0,0 +1,185 @@
using System.Collections;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 音乐通道播放器。
/// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。
/// </summary>
public class MusicChannelPlayer
{
readonly MusicContainerConfig m_containerConfig;
readonly MusicTransitionConfig m_transitionConfig;
readonly MonoBehaviour m_coroutineHost;
readonly ChannelFader m_fader;
// 当前正在播放的句柄
ContainerPlayHandle m_currentHandle;
uint m_currentContainerId;
float m_currentVolume = 1f;
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
MusicContainer m_currentContainer;
// 当前播放开始的时间(用于计算当前播到哪个拍子)
double m_playStartTime;
// 正在进行的 transition 协程(防止重叠)
Coroutine m_transitionCoroutine;
public MusicChannelPlayer(
MusicContainerConfig containerConfig,
MusicTransitionConfig transitionConfig,
MusicContainerPlayer player,
MonoBehaviour coroutineHost)
{
this.m_containerConfig = containerConfig;
this.m_transitionConfig = transitionConfig;
this.m_coroutineHost = coroutineHost;
this.m_fader = new ChannelFader(player, coroutineHost);
}
// ─────────────────────────────────────────────
// 公开接口
// ─────────────────────────────────────────────
/// <summary>
/// 切换到新的 Container。
/// fromPathId / toPathId 用于查询 Transition 配置。
/// </summary>
internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
{
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
return; // 已经在播目标,无需切换
MusicTransition transition = ResolveTransition(fromPathId, toPathId);
if (this.m_transitionCoroutine != null)
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
this.m_transitionCoroutine = this.m_coroutineHost.StartCoroutine(
DoTransition(newContainerId, transition));
}
/// <summary>
/// 立即停止当前播放(无淡出)
/// </summary>
public void Stop()
{
if (this.m_transitionCoroutine != null)
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
this.m_fader.StopCurrent();
}
// ─────────────────────────────────────────────
// Transition 流程
// ─────────────────────────────────────────────
IEnumerator DoTransition(uint newContainerId, MusicTransition transition)
{
// ── 1. 等待节拍对齐(由 Transition 的 AlignMode 决定)──
if (transition != null && this.m_currentContainer != null)
{
yield return this.m_coroutineHost.StartCoroutine(
WaitForAlignment(transition.AlignMode, this.m_currentContainer));
}
// ── 2 & 3. 淡出与淡入并行:两条分支从同一时刻起算各自的 Offset,互不等待 ──
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
float outVol = this.m_fader.CurrentVolume;
float fadeOutOffset = transition?.FadeOutOffset ?? 0f;
float fadeOutTime = transition?.FadeOutTime ?? 0f;
float fadeInOffset = transition?.FadeInOffset ?? 0f;
float fadeInTime = transition?.FadeInTime ?? 0f;
this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeOutBranch(outgoing, outVol, fadeOutOffset, fadeOutTime));
yield return this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeInBranch(newContainerId, fadeInOffset, fadeInTime,
onContainerStarted: () =>
{
// Music 独有:记录新 container 和播放起始时间
this.m_currentContainer = this.m_containerConfig.QueryById(newContainerId);
this.m_playStartTime = AudioSettings.dspTime;
}));
}
// ─────────────────────────────────────────────
// 节拍对齐等待
// ─────────────────────────────────────────────
IEnumerator WaitForAlignment(AlignMode mode, MusicContainer container)
{
if (mode == AlignMode.Immediate || container.Bpm <= 0f)
yield break;
double now = AudioSettings.dspTime;
double elapsed = now - this.m_playStartTime;
double secondsPerBeat = 60.0 / container.Bpm;
if (mode == AlignMode.Beat)
{
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 = ParseBeatsPerBar(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);
}
}
/// <summary>
/// 解析拍号字符串(如 "4/4", "3/4"),返回每小节拍数。
/// </summary>
static int ParseBeatsPerBar(string timeSig)
{
if (string.IsNullOrEmpty(timeSig)) return 4;
string[] parts = timeSig.Split('/');
if (parts.Length >= 1 && int.TryParse(parts[0], out int beats))
return beats;
return 4;
}
// ─────────────────────────────────────────────
// 工具
// ─────────────────────────────────────────────
/// <summary>
/// 查询 Transition 配置,支持精确匹配和 999 通配符。
/// ID 规则:FromPathId × 1000 + ToPathId999 表示任意。
/// </summary>
MusicTransition ResolveTransition(uint fromPathId, uint toPathId)
{
// 优先精确匹配
uint exactId = fromPathId * 1000 + toPathId;
MusicTransition exact = this.m_transitionConfig.QueryById(exactId);
if (exact != null) return exact;
// From 为任意
uint fromWildcard = 999u * 1000 + toPathId;
MusicTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
if (fromWild != null) return fromWild;
// To 为任意
uint toWildcard = fromPathId * 1000 + 999u;
MusicTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
if (toWild != null) return toWild;
// 全通配
const uint allWild = 999u * 1000 + 999u;
return this.m_transitionConfig.QueryById(allWild);
}
}
}