using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace OCES.Audio { /// /// 音乐通道播放器。 /// 负责:切换目标 Container、等待节拍对齐、执行淡入淡出 Transition。 /// class MusicChannelPlayer { readonly MusicContainerConfig m_containerConfig; readonly MusicTransitionConfig m_transitionConfig; readonly MusicSegmentConfig m_segmentConfig; readonly MonoBehaviour m_coroutineHost; readonly ChannelFader m_fader; readonly BeatClock m_beatClock; readonly List m_transitionCandidates = new(); Coroutine m_currentFadeInCoroutine; Coroutine m_currentFadeOutCoroutine; // 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐) MusicContainer m_currentContainer; // 正在进行的 transition 协程(防止重叠) Coroutine m_transitionCoroutine; internal MusicChannelPlayer( MusicContainerConfig containerConfig, MusicSegmentConfig segmentConfig, MusicTransitionConfig transitionConfig, LongAudioContainerPlayer player, MonoBehaviour coroutineHost, Action onBeat, Action onBar, Action onGrid) { this.m_containerConfig = containerConfig; this.m_transitionConfig = transitionConfig; this.m_segmentConfig = segmentConfig; this.m_coroutineHost = coroutineHost; this.m_fader = new ChannelFader(player, coroutineHost); this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid); player.OnBlendError += this.m_beatClock.OnBlendError; } // ───────────────────────────────────────────── // 公开接口 // ───────────────────────────────────────────── /// /// 切换到新的 Container。 /// internal void SwitchTo(uint newContainerId) { 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); this.m_transitionCoroutine = this.m_coroutineHost.StartCoroutine( DoTransition(newContainerId, transition)); } /// /// 立即停止当前播放(无淡出) /// internal void Stop() { this.m_beatClock.StopAll(); if (this.m_transitionCoroutine != null) this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine); this.m_fader.StopCurrent(); } // ───────────────────────────────────────────── // Transition 流程 // ───────────────────────────────────────────── IEnumerator DoTransition(uint newContainerId, MusicTransition transition) { if (this.m_currentFadeInCoroutine != null) { this.m_coroutineHost.StopCoroutine(this.m_currentFadeInCoroutine); this.m_currentFadeInCoroutine = null; } if (this.m_currentFadeOutCoroutine != null) { this.m_coroutineHost.StopCoroutine(this.m_currentFadeOutCoroutine); this.m_currentFadeOutCoroutine = null; } // ── 构建 SyncPoint 状态 ── SyncPointState syncState = new() { Mode = transition?.SyncPoint ?? SyncPoint.Start, Ready = false, }; //Debug.Log($"[MusicChannelPlayer] DoTransition: transition?.SyncPoint={transition?.SyncPoint}, Mode={syncState.Mode}"); if (syncState.Mode == SyncPoint.SameAsCurrentSegment) { 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 }) { Debug.LogWarning($"[MusicChannelPlayer] SyncPoint.SameAsCurrentSegment 不支持 Blend 类型 Container({this.m_currentContainer.Id}),降级为 Start"); syncState.Mode = SyncPoint.Start; } else { AudioSource oldSource = this.m_fader.CurrentHandle.GetFirstLeafSource(); if (oldSource && oldSource.clip) { syncState.BaseTimeSamples = oldSource.timeSamples; syncState.BaseDspTime = AudioSettings.dspTime; syncState.SampleRate = oldSource.clip.frequency; syncState.SourceStartOffset = GetEffectiveStartOffset(this.m_currentContainer); } else { Debug.LogWarning("[MusicChannelPlayer] SameAsCurrentSegment: 旧 Source 不存在,降级为 Start"); syncState.Mode = SyncPoint.Start; } } } // ── 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,互不等待 ── if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null) yield break; // 如果等待期间被切回来了,就不淡变了 ContainerPlayHandle outgoing = this.m_fader.CurrentHandle; float outVol = this.m_fader.CurrentVolume; this.m_currentFadeOutCoroutine = this.m_coroutineHost.StartCoroutine( this.m_fader.FadeOutBranch(outgoing, outVol, transition, syncState)); this.m_currentFadeInCoroutine = this.m_coroutineHost.StartCoroutine( this.m_fader.FadeInBranch(newContainerId, transition, syncState, onContainerStarted: () => { MusicContainer container = this.m_containerConfig.QueryById(newContainerId); float bpm = container.Bpm; double newStartOffset = GetEffectiveStartOffset(container); double dspTime; if (syncState is { Mode: SyncPoint.SameAsCurrentSegment }) { double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime; int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate); double currentAudioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate; // 当前的source播到了哪里 double currentLogicalTime = Math.Max(0, currentAudioTime - syncState.SourceStartOffset); // EntryCue之后播了多少秒。要是小于0说明pre-entry还没播完。 double newAudioTime = currentLogicalTime + newStartOffset; // 新Audio应该从多少秒开始播放 bool isPlayPreEntry = true; //TODO transition 是否播放pre-entry syncState.TargetAudioTime = isPlayPreEntry ? currentLogicalTime + newStartOffset : currentLogicalTime; dspTime = AudioSettings.dspTime - newAudioTime; } else { double startPlayTime; if (this.m_currentContainer == null) { startPlayTime = 0; } else { double fadeInTime = transition?.FadeInTime ?? 0f; startPlayTime = newStartOffset - fadeInTime; startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity); } syncState.StartPlayTime = startPlayTime; dspTime = AudioSettings.dspTime; } this.m_currentContainer = container; this.m_beatClock.Restart(container, bpm, dspTime, newStartOffset); })); yield return this.m_currentFadeInCoroutine; this.m_currentFadeInCoroutine = null; } // ───────────────────────────────────────────── // 节拍对齐等待 // ───────────────────────────────────────────── IEnumerator WaitForAlignment(AlignMode mode, MusicContainer container) { if (mode == AlignMode.Immediate || container.Bpm <= 0f) yield break; double target = this.m_beatClock.GetNextDspTime(mode); yield return new WaitUntil(() => AudioSettings.dspTime >= target); } // ───────────────────────────────────────────── // 工具 // ───────────────────────────────────────────── /// /// 查询 Transition 配置,支持精确匹配和 -1 通配符。 /// MusicTransition ResolveTransition(int sourceContainerId, int destinationContainerId) { this.m_transitionCandidates.Clear(); foreach (MusicTransition transition in this.m_transitionConfig.MusicTransitionList()) { bool sourceMatch = transition.SourceContainerID == sourceContainerId || transition.SourceContainerID < 0; bool destMatch = transition.DestinationContainerID == destinationContainerId || transition.DestinationContainerID < 0; if (sourceMatch && destMatch) { this.m_transitionCandidates.Add(transition); } } if (this.m_transitionCandidates.Count == 0) return this.m_transitionConfig.QueryById(1); MusicTransition best = this.m_transitionCandidates[0]; for (int i = 1; i < this.m_transitionCandidates.Count; i++) { if (this.m_transitionCandidates[i].Id > best.Id) best = this.m_transitionCandidates[i]; } return best; } double GetEffectiveStartOffset(MusicContainer container) { if (container.ContainerType == ContainerType.Blend) { return 0.0; } if (container.Segments is not { Count: > 0 }) return 0.0; uint firstId = container.Segments[0]; if (firstId < 1000000u) { MusicSegment segment = this.m_segmentConfig.QueryById(firstId); return segment?.StartOffset ?? 0.0; } MusicContainer child = this.m_containerConfig.QueryById(firstId); return child != null ? GetEffectiveStartOffset(child) : 0.0; } } }