refactor: 重构 Transition 查询逻辑,移除 PathId 改用 ContainerId 匹配
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"Rider": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "http://127.0.0.1:64342/sse",
|
||||||
|
"headers": {},
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# AGENTS.md — AudioSystem (OCES)
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Unity 2022.3.62f3, URP, C#. Custom audio & haptic middleware under the namespace **OCES**.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Assets/Scripts/OCES/
|
||||||
|
├── Audio/
|
||||||
|
│ ├── Generated/ ← AUTO-GENERATED, do not edit
|
||||||
|
│ └── HandWritten/ ← hand-written runtime code
|
||||||
|
│ ├── LongAudio/ ← music & ambience systems
|
||||||
|
│ └── Editor/ ← AudioImportTool
|
||||||
|
├── Haptic/
|
||||||
|
│ ├── Generated/ ← AUTO-GENERATED, do not edit
|
||||||
|
│ └── Handwritten/ ← hand-written runtime code
|
||||||
|
├── Metronome.cs ← demo/test helper
|
||||||
|
├── PlaySoundBind.cs ← UI bind helpers
|
||||||
|
├── SetStateBind.cs
|
||||||
|
└── SetPropertyBind.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated code boundary — critical
|
||||||
|
|
||||||
|
Everything under `Assets/Scripts/OCES/*/Generated/` is auto-generated from Excel data tables. Every file has the header comment: `auto generated by tools(注意:千万不要手动修改本文件)`.
|
||||||
|
|
||||||
|
- **Generated files**: data classes (`AudioObject`, `AudioGroup`, `MusicSegment`, etc.), config loader tables, `AudioConsts.cs` (Cues IDs, NameDictionaries, Parameters enums), `FileManager.cs`
|
||||||
|
- **If you need to change data**: edit the source `.xlsx` in `DataTables/`, then re-run the ExcelTool to regenerate `.bytes` configs + C# classes
|
||||||
|
- **Partial class pattern**: `AudioObject.cs` and config classes exist in both `Generated/` (serialization) and `HandWritten/` (runtime logic like switch resolution). Add runtime methods to the `HandWritten/` partial, never to `Generated/`
|
||||||
|
|
||||||
|
## Data pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
DataTables/*.xlsx (gitignored, source of truth for game data)
|
||||||
|
↓ ExcelTool
|
||||||
|
Resources/AudioData/*.bytes ← binary configs loaded at runtime via AudioConfigLoader
|
||||||
|
Resources/HapticData/*.bytes
|
||||||
|
↓ also generates
|
||||||
|
Assets/Scripts/OCES/*/Generated/*.cs ← C# data classes + AudioConsts
|
||||||
|
```
|
||||||
|
|
||||||
|
`Tools/` and `DataTables/` are gitignored. The Excel tool and source spreadsheets live outside version control.
|
||||||
|
|
||||||
|
## Duplicate types — intentional
|
||||||
|
|
||||||
|
`FileManager` and `IBinarySerializable` exist independently in both `OCES.Audio` and `OCES.Haptic` namespaces. They are not shared. When adding haptic code, use `OCES.Haptic.IBinarySerializable`; for audio, use `OCES.Audio.IBinarySerializable`.
|
||||||
|
|
||||||
|
## AudioMixer group paths
|
||||||
|
|
||||||
|
The mixer hierarchy is hard-referenced by path strings. These must match exactly:
|
||||||
|
|
||||||
|
- `Master/Regular/SFX` — default SFX group
|
||||||
|
- `Master/Regular/Voice` — voice group
|
||||||
|
- `Master/SFX_Accent` — accent SFX
|
||||||
|
- `Master/Regular/Music` — music pool
|
||||||
|
- `Master/Regular/SFX/Ambience` — ambience pool
|
||||||
|
|
||||||
|
## API conventions
|
||||||
|
|
||||||
|
- `AudioSystem.Instance.Play(uint audioId)` is the primary API. Use `Cues.Play_*` constants from `AudioConsts.cs`.
|
||||||
|
- `Play(string)` is `[Obsolete]` — string-based lookup is ambiguous for shared/duplicate names.
|
||||||
|
- `AudioSystem.Instance.SetState<TEnum>(TEnum state)` drives music/ambience switching. New state enums must be registered via `StateGroupRegistry.Register<TEnum>(typeId)` in `Parameters.EnumIds.RegisterAllGameState()`.
|
||||||
|
- `HapticSystem.Instance.Play(uint hapticId)` is typically called automatically by SfxSystem when an `AudioObject.Haptic` field is set; direct calls are for debugging only.
|
||||||
|
|
||||||
|
## Editor tools
|
||||||
|
|
||||||
|
- **AudioImportTool**: batch-applies import settings (sample rate, compression, load type) to all audio files in `Resources/Audios/`. Menu: `Tools/Audio/Apply Audio Import Settings`. CLI entry: `OCES.Audio.AudioImportTool.RunCli`.
|
||||||
|
- **ExcelTool** (commented out menu item): `Assets/Editor/ExcelTool.cs` — invokes external shell script to regenerate data from Excel. Currently disabled (`[UnityEditor.MenuItem]` is commented out).
|
||||||
|
|
||||||
|
## Third-party dependencies
|
||||||
|
|
||||||
|
- **DOTween** (`Assets/Scripts/DG/DOTween/`) — used for low-pass filter tweening and cross-fades
|
||||||
|
- **NiceVibrations / Lofelt** (`Assets/Scripts/Lofelt/NiceVibrations/`) — haptic feedback with native plugins per platform (`Assets/Plugins/{Android,iOS,macOS,Windows}/`)
|
||||||
|
|
||||||
|
## Audio file naming conventions
|
||||||
|
|
||||||
|
- `au_sfx_*` — sound effects
|
||||||
|
- `au_bgm_*` — background music
|
||||||
|
- `au_music_*` — music segments (used by the interactive music system)
|
||||||
|
- `au_stream.ogg` — streaming audio
|
||||||
|
|
||||||
|
## Resources paths
|
||||||
|
|
||||||
|
- Audio clips: `Resources/Audios/` (loaded via `Resources.Load<AudioClip>`)
|
||||||
|
- Audio configs: `Resources/AudioData/` (loaded via `AudioConfigLoader.Load<T>`)
|
||||||
|
- Haptic configs: `Resources/HapticData/`
|
||||||
|
- Haptic clips: `Resources/Haptics/` (`.haptic` files for advanced haptic playback)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,10 +13,22 @@ namespace OCES.Audio
|
|||||||
public partial class AmbienceTransition : IBinarySerializable
|
public partial class AmbienceTransition : IBinarySerializable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FromPathId x 1000 + ToPathId
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint Id { get; set; }
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从哪个Container出
|
||||||
|
/// 0 = None,-1 = 任意
|
||||||
|
/// </summary>
|
||||||
|
public int SourceContainerID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到哪个Container
|
||||||
|
/// 0 = None,-1 = 任意
|
||||||
|
/// </summary>
|
||||||
|
public int DestinationContainerID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 淡出总时长(s)
|
/// 淡出总时长(s)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -41,6 +53,8 @@ public partial class AmbienceTransition : IBinarySerializable
|
|||||||
public void DeSerialize(BinaryReader reader)
|
public void DeSerialize(BinaryReader reader)
|
||||||
{
|
{
|
||||||
Id = reader.ReadUInt32();
|
Id = reader.ReadUInt32();
|
||||||
|
SourceContainerID = reader.ReadInt32();
|
||||||
|
DestinationContainerID = reader.ReadInt32();
|
||||||
FadeOutTime = reader.ReadSingle();
|
FadeOutTime = reader.ReadSingle();
|
||||||
FadeOutOffset = reader.ReadSingle();
|
FadeOutOffset = reader.ReadSingle();
|
||||||
FadeInTime = reader.ReadSingle();
|
FadeInTime = reader.ReadSingle();
|
||||||
@@ -50,6 +64,8 @@ public partial class AmbienceTransition : IBinarySerializable
|
|||||||
public void Serialize(BinaryWriter writer)
|
public void Serialize(BinaryWriter writer)
|
||||||
{
|
{
|
||||||
writer.Write(Id);
|
writer.Write(Id);
|
||||||
|
writer.Write(SourceContainerID);
|
||||||
|
writer.Write(DestinationContainerID);
|
||||||
writer.Write(FadeOutTime);
|
writer.Write(FadeOutTime);
|
||||||
writer.Write(FadeOutOffset);
|
writer.Write(FadeOutOffset);
|
||||||
writer.Write(FadeInTime);
|
writer.Write(FadeInTime);
|
||||||
|
|||||||
@@ -13,10 +13,22 @@ namespace OCES.Audio
|
|||||||
public partial class MusicTransition : IBinarySerializable
|
public partial class MusicTransition : IBinarySerializable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FromPathId x 1000 + ToPathId
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint Id { get; set; }
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从哪个Container出
|
||||||
|
/// 0 = None,-1 = 任意
|
||||||
|
/// </summary>
|
||||||
|
public int SourceContainerID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到哪个Container
|
||||||
|
/// 0 = None,-1 = 任意
|
||||||
|
/// </summary>
|
||||||
|
public int DestinationContainerID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 淡出总时长(s)
|
/// 淡出总时长(s)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -60,6 +72,8 @@ public partial class MusicTransition : IBinarySerializable
|
|||||||
public void DeSerialize(BinaryReader reader)
|
public void DeSerialize(BinaryReader reader)
|
||||||
{
|
{
|
||||||
Id = reader.ReadUInt32();
|
Id = reader.ReadUInt32();
|
||||||
|
SourceContainerID = reader.ReadInt32();
|
||||||
|
DestinationContainerID = reader.ReadInt32();
|
||||||
FadeOutTime = reader.ReadSingle();
|
FadeOutTime = reader.ReadSingle();
|
||||||
FadeOutOffset = reader.ReadSingle();
|
FadeOutOffset = reader.ReadSingle();
|
||||||
FadeInTime = reader.ReadSingle();
|
FadeInTime = reader.ReadSingle();
|
||||||
@@ -72,6 +86,8 @@ public partial class MusicTransition : IBinarySerializable
|
|||||||
public void Serialize(BinaryWriter writer)
|
public void Serialize(BinaryWriter writer)
|
||||||
{
|
{
|
||||||
writer.Write(Id);
|
writer.Write(Id);
|
||||||
|
writer.Write(SourceContainerID);
|
||||||
|
writer.Write(DestinationContainerID);
|
||||||
writer.Write(FadeOutTime);
|
writer.Write(FadeOutTime);
|
||||||
writer.Write(FadeOutOffset);
|
writer.Write(FadeOutOffset);
|
||||||
writer.Write(FadeInTime);
|
writer.Write(FadeInTime);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace OCES.Audio
|
namespace OCES.Audio
|
||||||
@@ -12,6 +13,7 @@ namespace OCES.Audio
|
|||||||
readonly AmbienceTransitionConfig m_transitionConfig;
|
readonly AmbienceTransitionConfig m_transitionConfig;
|
||||||
readonly MonoBehaviour m_coroutineHost;
|
readonly MonoBehaviour m_coroutineHost;
|
||||||
readonly ChannelFader m_fader;
|
readonly ChannelFader m_fader;
|
||||||
|
readonly List<AmbienceTransition> m_transitionCandidates = new();
|
||||||
|
|
||||||
ContainerPlayHandle m_currentHandle;
|
ContainerPlayHandle m_currentHandle;
|
||||||
Coroutine m_transitionCoroutine;
|
Coroutine m_transitionCoroutine;
|
||||||
@@ -22,7 +24,7 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
internal AmbienceChannelPlayer(
|
internal AmbienceChannelPlayer(
|
||||||
AmbienceTransitionConfig transitionConfig,
|
AmbienceTransitionConfig transitionConfig,
|
||||||
MusicContainerPlayer player,
|
LongAudioContainerPlayer player,
|
||||||
MonoBehaviour coroutineHost)
|
MonoBehaviour coroutineHost)
|
||||||
{
|
{
|
||||||
this.m_transitionConfig = transitionConfig;
|
this.m_transitionConfig = transitionConfig;
|
||||||
@@ -34,12 +36,12 @@ namespace OCES.Audio
|
|||||||
// 公开接口
|
// 公开接口
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
|
internal void SwitchTo(uint newContainerId)
|
||||||
{
|
{
|
||||||
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AmbienceTransition transition = ResolveTransition(fromPathId, toPathId);
|
AmbienceTransition transition = ResolveTransition((int)this.m_currentContainerId, (int)newContainerId);
|
||||||
|
|
||||||
if (this.m_transitionCoroutine != null)
|
if (this.m_transitionCoroutine != null)
|
||||||
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
|
||||||
@@ -93,26 +95,29 @@ namespace OCES.Audio
|
|||||||
// 工具
|
// 工具
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
AmbienceTransition ResolveTransition(uint fromPathId, uint toPathId)
|
AmbienceTransition ResolveTransition(int sourceContainerId, int destinationContainerId)
|
||||||
{
|
{
|
||||||
// 优先精确匹配
|
this.m_transitionCandidates.Clear();
|
||||||
uint exactId = fromPathId * 1000 + toPathId;
|
foreach (AmbienceTransition transition in this.m_transitionConfig.AmbienceTransitionList())
|
||||||
AmbienceTransition exact = this.m_transitionConfig.QueryById(exactId);
|
{
|
||||||
if (exact != null) return exact;
|
bool sourceMatch = transition.SourceContainerID == sourceContainerId || transition.SourceContainerID < 0;
|
||||||
|
bool destMatch = transition.DestinationContainerID == destinationContainerId || transition.DestinationContainerID < 0;
|
||||||
|
if (sourceMatch && destMatch)
|
||||||
|
{
|
||||||
|
this.m_transitionCandidates.Add(transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// From 为任意
|
if (this.m_transitionCandidates.Count == 0)
|
||||||
uint fromWildcard = 999u * 1000 + toPathId;
|
return this.m_transitionConfig.QueryById(1);
|
||||||
AmbienceTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
|
|
||||||
if (fromWild != null) return fromWild;
|
|
||||||
|
|
||||||
// To 为任意
|
AmbienceTransition best = this.m_transitionCandidates[0];
|
||||||
uint toWildcard = fromPathId * 1000 + 999u;
|
for (int i = 1; i < this.m_transitionCandidates.Count; i++)
|
||||||
AmbienceTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
|
{
|
||||||
if (toWild != null) return toWild;
|
if (this.m_transitionCandidates[i].Id > best.Id)
|
||||||
|
best = this.m_transitionCandidates[i];
|
||||||
// 全通配
|
}
|
||||||
const uint allWild = 999u * 1000 + 999u;
|
return best;
|
||||||
return this.m_transitionConfig.QueryById(allWild);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ namespace OCES.Audio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChannelFader
|
public class ChannelFader
|
||||||
{
|
{
|
||||||
readonly MusicContainerPlayer m_player;
|
readonly LongAudioContainerPlayer m_player;
|
||||||
readonly MonoBehaviour m_coroutineHost;
|
readonly MonoBehaviour m_coroutineHost;
|
||||||
|
|
||||||
internal ContainerPlayHandle CurrentHandle { get; private set; }
|
internal ContainerPlayHandle CurrentHandle { get; private set; }
|
||||||
public uint CurrentContainerId { get; private set; }
|
public uint CurrentContainerId { get; private set; }
|
||||||
public float CurrentVolume { get; private set; }
|
public float CurrentVolume { get; private set; }
|
||||||
|
|
||||||
internal ChannelFader(MusicContainerPlayer player, MonoBehaviour coroutineHost)
|
internal ChannelFader(LongAudioContainerPlayer player, MonoBehaviour coroutineHost)
|
||||||
{
|
{
|
||||||
this.m_player = player;
|
this.m_player = player;
|
||||||
this.m_coroutineHost = coroutineHost;
|
this.m_coroutineHost = coroutineHost;
|
||||||
|
|||||||
+6
-6
@@ -12,7 +12,7 @@ namespace OCES.Audio
|
|||||||
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
|
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
|
||||||
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
|
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class MusicContainerPlayer
|
class LongAudioContainerPlayer
|
||||||
{
|
{
|
||||||
readonly MusicContainerConfig m_containerConfig;
|
readonly MusicContainerConfig m_containerConfig;
|
||||||
readonly MusicSegmentConfig m_segmentConfig;
|
readonly MusicSegmentConfig m_segmentConfig;
|
||||||
@@ -33,7 +33,7 @@ namespace OCES.Audio
|
|||||||
// Random 模式的不放回历史,key = containerId
|
// Random 模式的不放回历史,key = containerId
|
||||||
readonly Dictionary<uint, HashSet<uint>> m_randomHistory = new();
|
readonly Dictionary<uint, HashSet<uint>> m_randomHistory = new();
|
||||||
|
|
||||||
public MusicContainerPlayer(
|
public LongAudioContainerPlayer(
|
||||||
MusicContainerConfig containerConfig,
|
MusicContainerConfig containerConfig,
|
||||||
MusicSegmentConfig segmentConfig,
|
MusicSegmentConfig segmentConfig,
|
||||||
AudioSourcePool pool,
|
AudioSourcePool pool,
|
||||||
@@ -58,7 +58,7 @@ namespace OCES.Audio
|
|||||||
MusicContainer container = this.m_containerConfig.QueryById(containerId);
|
MusicContainer container = this.m_containerConfig.QueryById(containerId);
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[MusicContainerPlayer] 找不到 ContainerId: {containerId}");
|
Debug.LogError($"[LongAudioContainerPlayer] 找不到 ContainerId: {containerId}");
|
||||||
onFinished?.Invoke();
|
onFinished?.Invoke();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ namespace OCES.Audio
|
|||||||
Action onFinished)
|
Action onFinished)
|
||||||
{
|
{
|
||||||
float effectiveBpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
|
float effectiveBpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
|
||||||
//Debug.Log($"[MusicContainerPlayer] OnContainerEntered firing, container={container.Id}, subscribers={OnContainerEntered != null}");
|
//Debug.Log($"[LongAudioContainerPlayer] OnContainerEntered firing, container={container.Id}, subscribers={OnContainerEntered != null}");
|
||||||
OnContainerEntered?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
|
OnContainerEntered?.Invoke(container, effectiveBpm, AudioSettings.dspTime);
|
||||||
|
|
||||||
int loopsCompleted = 0;
|
int loopsCompleted = 0;
|
||||||
@@ -314,7 +314,7 @@ namespace OCES.Audio
|
|||||||
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
|
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
|
||||||
if (segment == null)
|
if (segment == null)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[MusicContainerPlayer] 找不到 SegmentId: {segmentId}");
|
Debug.LogError($"[LongAudioContainerPlayer] 找不到 SegmentId: {segmentId}");
|
||||||
onFinished?.Invoke();
|
onFinished?.Invoke();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ namespace OCES.Audio
|
|||||||
AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
|
AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
|
||||||
if (!clip)
|
if (!clip)
|
||||||
{
|
{
|
||||||
Debug.LogError($"[MusicContainerPlayer] 音频文件未找到: {segment.Name}");
|
Debug.LogError($"[LongAudioContainerPlayer] 音频文件未找到: {segment.Name}");
|
||||||
onFinished?.Invoke();
|
onFinished?.Invoke();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace OCES.Audio
|
namespace OCES.Audio
|
||||||
@@ -15,27 +17,21 @@ namespace OCES.Audio
|
|||||||
readonly MonoBehaviour m_coroutineHost;
|
readonly MonoBehaviour m_coroutineHost;
|
||||||
readonly ChannelFader m_fader;
|
readonly ChannelFader m_fader;
|
||||||
readonly BeatClock m_beatClock;
|
readonly BeatClock m_beatClock;
|
||||||
|
readonly List<MusicTransition> m_transitionCandidates = new();
|
||||||
|
|
||||||
Coroutine m_currentFadeInCoroutine;
|
Coroutine m_currentFadeInCoroutine;
|
||||||
Coroutine m_currentFadeOutCoroutine;
|
Coroutine m_currentFadeOutCoroutine;
|
||||||
|
|
||||||
// 当前正在播放的句柄
|
|
||||||
ContainerPlayHandle m_currentHandle;
|
|
||||||
uint m_currentContainerId;
|
|
||||||
|
|
||||||
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
|
// 当前播放的 Container(用于读取 bpm/timeSig 做节拍对齐)
|
||||||
MusicContainer m_currentContainer;
|
MusicContainer m_currentContainer;
|
||||||
|
|
||||||
// 当前播放开始的时间(用于计算当前播到哪个拍子)
|
|
||||||
double m_playStartTime;
|
|
||||||
|
|
||||||
// 正在进行的 transition 协程(防止重叠)
|
// 正在进行的 transition 协程(防止重叠)
|
||||||
Coroutine m_transitionCoroutine;
|
Coroutine m_transitionCoroutine;
|
||||||
|
|
||||||
internal MusicChannelPlayer(
|
internal MusicChannelPlayer(
|
||||||
MusicContainerConfig containerConfig,
|
MusicContainerConfig containerConfig,
|
||||||
MusicTransitionConfig transitionConfig,
|
MusicTransitionConfig transitionConfig,
|
||||||
MusicContainerPlayer player,
|
LongAudioContainerPlayer player,
|
||||||
MonoBehaviour coroutineHost,
|
MonoBehaviour coroutineHost,
|
||||||
Action<uint> onBeat,
|
Action<uint> onBeat,
|
||||||
Action<uint> onBar,
|
Action<uint> onBar,
|
||||||
@@ -56,14 +52,14 @@ namespace OCES.Audio
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 切换到新的 Container。
|
/// 切换到新的 Container。
|
||||||
/// fromPathId / toPathId 用于查询 Transition 配置。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
|
internal void SwitchTo(uint newContainerId)
|
||||||
{
|
{
|
||||||
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
|
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null)
|
||||||
return; // 已经在播目标,无需切换
|
return; // 已经在播目标,无需切换
|
||||||
|
|
||||||
MusicTransition transition = ResolveTransition(fromPathId, toPathId);
|
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);
|
||||||
@@ -181,7 +177,6 @@ namespace OCES.Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.m_currentContainer = container;
|
this.m_currentContainer = container;
|
||||||
this.m_playStartTime = dspTime;
|
|
||||||
this.m_beatClock.Restart(container, bpm, dspTime);
|
this.m_beatClock.Restart(container, bpm, dspTime);
|
||||||
}));
|
}));
|
||||||
yield return this.m_currentFadeInCoroutine;
|
yield return this.m_currentFadeInCoroutine;
|
||||||
@@ -206,29 +201,31 @@ namespace OCES.Audio
|
|||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询 Transition 配置,支持精确匹配和 999 通配符。
|
/// 查询 Transition 配置,支持精确匹配和 -1 通配符。
|
||||||
/// ID 规则:FromPathId × 1000 + ToPathId,999 表示任意。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
MusicTransition ResolveTransition(uint fromPathId, uint toPathId)
|
MusicTransition ResolveTransition(int sourceContainerId, int destinationContainerId)
|
||||||
{
|
{
|
||||||
// 优先精确匹配
|
this.m_transitionCandidates.Clear();
|
||||||
uint exactId = fromPathId * 1000 + toPathId;
|
foreach (MusicTransition transition in this.m_transitionConfig.MusicTransitionList())
|
||||||
MusicTransition exact = this.m_transitionConfig.QueryById(exactId);
|
{
|
||||||
if (exact != null) return exact;
|
bool sourceMatch = transition.SourceContainerID == sourceContainerId || transition.SourceContainerID < 0;
|
||||||
|
bool destMatch = transition.DestinationContainerID == destinationContainerId || transition.DestinationContainerID < 0;
|
||||||
|
if (sourceMatch && destMatch)
|
||||||
|
{
|
||||||
|
this.m_transitionCandidates.Add(transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// From 为任意
|
if (this.m_transitionCandidates.Count == 0)
|
||||||
uint fromWildcard = 999u * 1000 + toPathId;
|
return this.m_transitionConfig.QueryById(1);
|
||||||
MusicTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
|
|
||||||
if (fromWild != null) return fromWild;
|
|
||||||
|
|
||||||
// To 为任意
|
MusicTransition best = this.m_transitionCandidates[0];
|
||||||
uint toWildcard = fromPathId * 1000 + 999u;
|
for (int i = 1; i < this.m_transitionCandidates.Count; i++)
|
||||||
MusicTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
|
{
|
||||||
if (toWild != null) return toWild;
|
if (this.m_transitionCandidates[i].Id > best.Id)
|
||||||
|
best = this.m_transitionCandidates[i];
|
||||||
// 全通配
|
}
|
||||||
const uint allWild = 999u * 1000 + 999u;
|
return best;
|
||||||
return this.m_transitionConfig.QueryById(allWild);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ namespace OCES.Audio
|
|||||||
internal event Action<uint> OnBar;
|
internal event Action<uint> OnBar;
|
||||||
internal event Action<uint> OnGrid;
|
internal event Action<uint> OnGrid;
|
||||||
|
|
||||||
// 记录上一次两个通道各自匹配到的 PathId,用于查 Transition 表
|
|
||||||
uint m_lastMusicPathId;
|
|
||||||
uint m_lastAmbiencePathId;
|
|
||||||
|
|
||||||
internal IReadOnlyDictionary<Type, Enum> ActiveStates
|
internal IReadOnlyDictionary<Type, Enum> ActiveStates
|
||||||
{
|
{
|
||||||
@@ -37,12 +35,12 @@ namespace OCES.Audio
|
|||||||
AudioSourcePool musicPool,
|
AudioSourcePool musicPool,
|
||||||
AudioSourcePool ambiencePool)
|
AudioSourcePool ambiencePool)
|
||||||
{
|
{
|
||||||
MusicContainerPlayer musicContainerPlayer = new(containers, segments, musicPool, this);
|
LongAudioContainerPlayer longAudioContainerPlayer = new(containers, segments, musicPool, this);
|
||||||
MusicContainerPlayer ambientContainerPlayer = new(containers, segments, ambiencePool, this);
|
LongAudioContainerPlayer ambientContainerPlayer = new(containers, segments, ambiencePool, this);
|
||||||
|
|
||||||
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
|
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
|
||||||
this.m_musicChannel = new MusicChannelPlayer(
|
this.m_musicChannel = new MusicChannelPlayer(
|
||||||
containers, musicTransitions, musicContainerPlayer, this,
|
containers, musicTransitions, longAudioContainerPlayer, this,
|
||||||
id => OnBeat?.Invoke(id),
|
id => OnBeat?.Invoke(id),
|
||||||
id => OnBar?.Invoke(id),
|
id => OnBar?.Invoke(id),
|
||||||
id => OnGrid?.Invoke(id));
|
id => OnGrid?.Invoke(id));
|
||||||
@@ -61,14 +59,8 @@ namespace OCES.Audio
|
|||||||
out uint musicContainerId,
|
out uint musicContainerId,
|
||||||
out uint ambienceContainerId);
|
out uint ambienceContainerId);
|
||||||
|
|
||||||
uint newMusicPathId = this.m_stateRouter.LastMusicPathId;
|
this.m_musicChannel.SwitchTo(musicContainerId);
|
||||||
uint newAmbiencePathId = this.m_stateRouter.LastAmbiencePathId;
|
this.m_ambienceChannel.SwitchTo(ambienceContainerId);
|
||||||
|
|
||||||
this.m_musicChannel.SwitchTo(musicContainerId, this.m_lastMusicPathId, newMusicPathId);
|
|
||||||
this.m_ambienceChannel.SwitchTo(ambienceContainerId, this.m_lastAmbiencePathId, newAmbiencePathId);
|
|
||||||
|
|
||||||
this.m_lastMusicPathId = newMusicPathId;
|
|
||||||
this.m_lastAmbiencePathId = newAmbiencePathId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user