diff --git a/AGENTS.md b/AGENTS.md index 9c1c708..1311737 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,31 +4,76 @@ Unity 2022.3.62f3, URP, C#. Custom audio & haptic middleware under the namespace **OCES**. +--- + ## Architecture ``` Assets/Scripts/OCES/ +├── ResourceLoader.cs ← 资源缓存与同步/异步加载 +├── Metronome.cs ← demo/test helper +├── PlaySoundBind.cs ← UI bind helpers +├── SetStateBind.cs ← 状态切换 bind +├── SetPropertyBind.cs ← 属性控制 bind +│ ├── Audio/ -│ ├── Generated/ ← AUTO-GENERATED, do not edit -│ └── HandWritten/ ← hand-written runtime code -│ ├── LongAudio/ ← music & ambience systems -│ └── Editor/ ← AudioImportTool +│ ├── Generated/ ← AUTO-GENERATED, do not edit +│ │ ├── AudioObject.cs ← AudioObject + AudioObjectConfig(数据类 + 加载表) +│ │ ├── AudioGroup.cs ← AudioGroup + AudioGroupConfig +│ │ ├── AudioConsts.cs ← Cues.Play_* 常量, NameDictionaries, Parameters(GameState enum + EnumIds) +│ │ ├── MusicSegment.cs ← MusicSegment + MusicSegmentConfig +│ │ ├── MusicContainer.cs ← MusicContainer + MusicContainerConfig +│ │ ├── MusicPath.cs ← MusicPath + MusicPathConfig +│ │ ├── MusicTransition.cs ← MusicTransition + MusicTransitionConfig +│ │ ├── AmbiencePath.cs ← AmbiencePath + AmbiencePathConfig +│ │ ├── AmbienceTransition.cs ← AmbienceTransition + AmbienceTransitionConfig +│ │ └── FileManager.cs ← FileManager (OCES.Audio 命名空间, IBinarySerializable + 字节读取) +│ │ +│ └── HandWritten/ ← hand-written runtime code +│ ├── AudioSystem.cs ← 主入口:持有 SfxSystem + MusicSystem,对外暴露 Play / SetState / SetLowpass +│ ├── SfxSystem.cs ← 音效系统:节流/并发/优先级/打断、连续容器播放、ActiveSound 管理 +│ ├── AudioSourcePool.cs ← AudioSource 对象池 +│ ├── AudioObject.cs ← partial: AudioObjectConfig 的 Switch 解析逻辑 +│ ├── AudioExtendSettings.cs ← 集中配置 ScriptableObject(路径/Mixer组/导入参数/淡出淡入曲线等) +│ ├── HandWrittenDefinitions.cs ← 枚举定义(KillMode, MixingType, ContainerType, AlignMode, CallbackFlags, 等)+ IBinarySerializable 接口 + IPathEntry/ITransitionConfig 接口 +│ ├── DebugInfoCollector.cs ← 运行时调试信息收集(仅在 UNITY_EDITOR || DEVELOPMENT_BUILD 编译) +│ ├── VolumeStepResolver.cs ← 音量阶梯变化(VolumeStep)计算 +│ ├── PitchStepResolver.cs ← 音高阶梯变化(PitchStep)计算 +│ ├── LongAudio/ ← interactive music & ambience systems +│ │ ├── MusicSystem.cs ← 音乐/环境音总控,持有 MusicChannelPlayer + AmbienceChannelPlayer,由 AudioSystem 驱动 +│ │ ├── MusicStateRouter.cs ← 基于路径规则的全量状态匹配路由 + StateGroupRegistry 注册表 +│ │ ├── MusicChannelPlayer.cs ← 音乐通道:Transition 切换、节拍对齐、淡入淡出 +│ │ ├── AmbienceChannelPlayer.cs ← 环境音通道:Transition 切换、淡入淡出(无节拍对齐) +│ │ ├── LongAudioContainerPlayer.cs ← Container 递归播放引擎,被两个 Channel 共用 +│ │ ├── ChannelFader.cs ← 音量淡入淡出 + SyncPoint 同步点管理 +│ │ ├── BeatClock.cs ← 节拍/小节/Grid 定时回调 +│ │ ├── AudioContainerSelector.cs ← SFX Container 的随机/顺序选择(含 LimitRepetition) +│ │ ├── MusicSegment.cs ← partial: MusicSegment.IsOffBeat + MusicSegmentConfig.Validate() +│ │ └── MusicContainer.cs ← partial: MusicContainerConfig 的 GetBeatsPerBar/Validate +│ └── Editor/ ← Editor-only tools +│ ├── AudioImportTool.cs ← 批量音频导入设置 +│ └── AudioExtendSettingsProvider.cs ← Project Settings 面板 "Audio Extend" +│ ├── 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/ ← AUTO-GENERATED, do not edit +│ │ └── HapticObject.cs ← HapticObject + HapticObjectConfig(数据类 + 加载表) +│ │ +│ └── Handwritten/ ← hand-written runtime code +│ ├── HapticSystem.cs ← 触感反馈主系统(Preset / Emphasis / Constant / Advance 四种模式) +│ ├── HapticSystemSettings.cs ← 触感配置 ScriptableObject(资源路径) +│ └── HandwrittenDefinitions.cs ← IBinarySerializable 接口 + FileManager + HapticType 枚举 ``` +--- + ## 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` +- **Generated files**: data classes (`AudioObject`, `AudioGroup`, `MusicSegment`, `MusicContainer`, `MusicPath`, `MusicTransition`, `AmbiencePath`, `AmbienceTransition`, `HapticObject`), config loader tables (`*Config` classes), `AudioConsts.cs` (Cues IDs, NameDictionaries, Parameters enums), `FileManager.cs` (in `OCES.Audio` namespace) - **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/` +- **Partial class pattern**: `AudioObject.cs`, `AudioObjectConfig`, `MusicSegment`, `MusicSegmentConfig`, `MusicContainer`, `MusicContainerConfig` exist in both `Generated/` (serialization) and `HandWritten/` (runtime logic). Add runtime methods to the `HandWritten/` partial, never to `Generated/`. +- **Generated data protocols**: Generated `MusicPath` and `AmbiencePath` classes have their `ITransitionConfig` / `IPathEntry` interface implementations added via `HandWrittenDefinitions.cs` using `partial class` declarations. ## Data pipeline @@ -43,37 +88,227 @@ Assets/Scripts/OCES/*/Generated/*.cs ← C# data classes + AudioConsts `Tools/` and `DataTables/` are gitignored. The Excel tool and source spreadsheets live outside version control. +## Configuration: AudioExtendSettings + +`AudioExtendSettings` is a **ScriptableObject singleton** that centralises all configurable parameters for the audio system. It replaces hardcoded path strings and magic numbers. + +- **Asset location**: `Assets/Settings/AudioExtendSettings.asset` +- **Editor entry**: `Project Settings > Audio Extend` (via `AudioExtendSettingsProvider`) +- **Creation**: `Tools/Audio/Create AudioExtendSettings Asset` (menu item) +- **Runtime injection**: `AudioSystem.Awake()` reads the `[SerializeField]` inspector reference and assigns it to `AudioExtendSettings.Instance` + +### Configured parameters + +| Section | Key fields | +|---------|-----------| +| Resource Paths | `audioConfigPath`, `audioResourcePath`, `audioMixerPath` | +| AudioMixer Groups | `sfxGroupPath`, `voiceGroupPath`, `accentSfxGroupPath`, `musicGroupPath`, `ambienceGroupPath` | +| Lowpass Filter | `lowpassParamName`, `lowpassEnabledCutoff` (440Hz), `lowpassDisabledCutoff` (22000Hz), `lowpassTweenDuration` | +| Audio Import | `compressionFormat`, `sfxSampleRate` (22050), `musicSampleRate` (44100), `musicQuality`, `sfxQuality`, `decompressThreshold` (5s), `streamingThreshold` (15s) | +| Fade | `defaultFadeOutEase` (InSine), `defaultFadeInEase` (OutSine) | + +**All code must read mixer group paths from `AudioExtendSettings.Instance.*GroupPath`** — do not hardcode path strings. + ## 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`. +`FileManager` and `IBinarySerializable` exist independently in both `OCES.Audio` and `OCES.Haptic` namespaces. They are not shared. -## AudioMixer group paths +- **Audio**: `OCES.Audio.IBinarySerializable` and `OCES.Audio.FileManager` are defined in `Audio/Generated/FileManager.cs` +- **Haptic**: `OCES.Haptic.IBinarySerializable` and `OCES.Haptic.FileManager` are defined in `Haptic/Handwritten/HandwrittenDefinitions.cs` -The mixer hierarchy is hard-referenced by path strings. These must match exactly: +When adding haptic code, use `OCES.Haptic.*`; for audio, use `OCES.Audio.*`. -- `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 +## ResourceLoader + +`OCES.ResourceLoader` at the OCES root is a shared resource caching layer used by both `AudioSystem` and `HapticSystem`. It provides: + +- `LoadSync(string path)` — cached synchronous load via `Resources.Load` +- `LoadAsync(string path, Action onComplete)` — async load with callback deduplication (concurrent requests for the same path share one load operation) +- Injected into `AudioSystem` and `HapticSystem` during `Awake()` + +Both `AudioConfigLoader` and `HapticConfigLoader` use `ResourceLoader` to load `TextAsset` config bytes. ## 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 state)` drives music/ambience switching. New state enums must be registered via `StateGroupRegistry.Register(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. +### AudioSystem — primary entry point + +```csharp +// Core playback — prefer uint overloads +AudioSystem.Instance.Play(uint audioId); // via Cues.Play_* constants +AudioSystem.Instance.Play(int audioId); // convenience, casts to uint +AudioSystem.Instance.Play(AudioObject audioObject); // direct object (resolves Switch containers) + +// State-driven music/ambience switching +AudioSystem.Instance.SetState(TEnum state); // drives MusicStateRouter → channel switching + +// Lowpass filter +AudioSystem.Instance.SetLowpass(bool enable); // tweens mixer lowpass via DOTween + +// Music sync callbacks (subscribed via events) +AudioSystem.Instance.OnBeat += containerId => { }; +AudioSystem.Instance.OnBar += containerId => { }; +AudioSystem.Instance.OnGrid += containerId => { }; +``` + +- `Play(string)` is `[Obsolete]` — string-based lookup is ambiguous for shared/duplicate names. It survives for backward compatibility only. +- `SetState` updates the `MusicStateRouter` which matches `MusicPath` / `AmbiencePath` tables and switches the appropriate Container via `MusicChannelPlayer` / `AmbienceChannelPlayer`. +- **New state enums**: register via `StateGroupRegistry.Register(typeId)` in `Parameters.EnumIds.RegisterAllGameState()` (generated in `AudioConsts.cs`). + +### SfxSystem — internal SFX runtime + +`SfxSystem` is held by `AudioSystem` and not exposed publicly, but its methods are accessible via `AudioSystem` internally: + +- `Stop(uint audioId)` — stop all active instances of a given audio ID +- `SetVolume(uint audioId, float targetVolume)` — live volume adjustment +- `SetPitch(uint audioId, float targetPitch)` — live pitch adjustment + +SfxSystem manages: throttle (`ThrottleCount`), min interval (`MinInterval`), priority preemption (`KillMode`), concurrent instance tracking, continuous container playback, and haptic auto-trigger. + +### HapticSystem + +- `HapticSystem.Instance.Play(uint hapticId)` — called automatically by SfxSystem when `AudioObject.Haptic` is set; direct calls log a warning (debug-only). +- `HapticSystem.Instance.Stop(uint? hapticId = null)` — stops current haptic playback +- Supports four `HapticType`s: `Preset`, `Emphasis`, `Constant`, `Advance` (`.haptic` file) + +--- + +## Interactive Music System + +### State-driven routing + +1. `AudioSystem.SetState(state)` → forwards to `MusicSystem.OnStateChanged(state)` +2. `MusicStateRouter.SetState()` stores the state, then performs full-match against `MusicPath` and `AmbiencePath` tables +3. Path format: `TypeID1,子状态|TypeID2,子状态…` — all conditions must be met (AND logic). Priority field selects the best match when multiple paths satisfy. +4. Best-matched `ContainerId` is dispatched to `MusicChannelPlayer.SwitchTo()` and `AmbienceChannelPlayer.SwitchTo()` + +Key class: `MusicStateRouter` — maintains `ActiveStates` dictionary, exposes `LastMusicPathId` / `LastAmbiencePathId` for Transition lookup. + +### StateGroupRegistry + +Programmer-maintained enum-to-TypeId mapping. Usage: + +```csharp +// In Parameters.EnumIds.RegisterAllGameState() (generated): +StateGroupRegistry.Register(1); +``` + +The integer `1` must match the TypeId values in the MusicPath/AmbiencePath Excel tables. + +### Transition system + +- `MusicChannelPlayer` handles music Transitions with **beat-aligned switching** (supports `AlignMode.Immediate` / `Beat` / `Bar`) +- `AmbienceChannelPlayer` handles ambience Transitions with **simple cross-fade** (no beat alignment) +- Both use `ChannelFader` for volume ramping via DG.Tweening `DOFade` +- Transition resolution: matches `SourceContainerId → DestinationContainerId` with `-1` wildcard support; highest-ID exact match wins +- `SyncPoint.SameAsCurrentSegment` allows new music to start at the same playback position as the outgoing track + +### BeatClock + +Drives music-sync callbacks (`OnBeat`, `OnBar`, `OnGrid`) using `AudioSettings.dspTime`: + +- Parses BPM from `MusicContainer.Bpm` (falls back to inherited BPM from parent Container) +- Parses time signature from `MusicContainer.TimeSig` (e.g. `"4/4"`) +- Grid interval from `MusicContainer.Grid` (in bars) +- Automatically disables callbacks when Blend containers have multiple divergent BPMs + +### Container playback + +`LongAudioContainerPlayer` recursively plays a `MusicContainer` tree and supports: + +- **Container types**: `Random`, `Sequence`, `Blend` +- **Random mode**: shuffle (不放回) with optional no-repeat history +- **Sequence mode**: global step cursor per container +- **Blend mode**: simultaneous playback of all child segments +- **Off-beat detection**: `MusicSegmentConfig.Validate()` checks `StartOffset`/`EndOffset` against actual clip lengths +- Returns `ContainerPlayHandle` for external stop/fade control + +--- + +## SFX Runtime Logic + +### Container playback + +AudioObject supports container types (`ContainerType`): + +| Type | Behavior | +|------|----------| +| `Random` | Random selection from child clips, with `LimitRepetition` and `RandomType` (Standard / Shuffle 不放回) | +| `Sequence` | Step-through in order | +| `Blend` | (reserved for long audio) | +| `Switch` | State-driven selection via `SwitchGroupId` — resolved by `AudioObjectConfig.PreParseSwitchMappings()` | + +`ContainerPlayMode`: +- `false` (Step): play one clip at a time +- `true` (Continuous): play all clips in sequence with gap, supports Random/Sequence + +### Selection helpers + +- `AudioContainerSelector` — manages random histories, sequence cursors, `LimitRepetition` queues. Shared across all SFX instances. +- `PitchStepResolver` — incremental pitch shifting per `PitchStep` (+ semitones) within `PitchStepThreshold` (ms), clamped by `PitchStepLimit` +- `VolumeStepResolver` — incremental volume adjustment per `VolumeStep` (dB) within `VolumeStepThreshold` (ms) + +### Priority & preemption (KillMode) + +When `ThrottleCount` or `Group` limits are exceeded: + +- `KillMode.Oldest` — evict the oldest playing instance +- `KillMode.Newest` — silence the newest (incoming) instance + +### Mixing groups (MixingType) + +| Enum | Mixer path (from AudioExtendSettings) | +|------|--------------------------------------| +| `Sfx` | `sfxGroupPath` | +| `Voice` | `voiceGroupPath` | +| `Accented` | `accentSfxGroupPath` | + +### ActiveSound + +Tracks each playing sound instance: `AudioSource`, `AudioObject`, `StartTime`, `CurrentLoopCount`, `Coroutine`, `Pitch`, `Volume`, `State` (`Pending` / `Playing` / `Finished`). + +--- ## 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). +### AudioImportTool + +Batch-applies import settings to all `.wav`/`.ogg` files in the audio resources directory. Now fully configuration-driven via `AudioExtendSettings`. + +- **Menu**: `Tools/Audio/Apply Audio Import Settings` +- **CLI entry**: `OCES.Audio.AudioImportTool.RunCli` (via `-executeMethod`) +- **Classification**: Auto-detects Music/SFX/Voice by checking `MusicSegment` table → `AudioObject` table `MixingType` → filename heuristics +- **Import rules** (all from `AudioExtendSettings`): + - SFX/Voice: `sfxSampleRate`, DecompressOnLoad (or Streaming if ≥ `streamingThreshold`) + - Music: `musicSampleRate`, DecompressOnLoad (≤ `decompressThreshold`), Streaming (≥ `streamingThreshold`), CompressedInMemory (between) + - Files referenced by `AudioObject.Haptic` force `preloadAudioData = true` + +### AudioExtendSettingsProvider + +Provides a custom Project Settings tab (`Project Settings > Audio Extend`) for editing `AudioExtendSettings.asset` with full serialized property inspection. + +### ExcelTool + +`Assets/Editor/ExcelTool.cs` — invokes external shell script to regenerate data from Excel. Currently disabled (`[UnityEditor.MenuItem]` is commented out). + +--- + +## Debugging + +`DebugInfoCollector` (compiled only under `UNITY_EDITOR || DEVELOPMENT_BUILD`): + +- Displays current active states, clip concurrent counts, and active sound list to a `UI.Text` component +- Updated every frame by `SfxSystem.Update()` and `MusicStateRouter.SetState()` +- Singleton pattern, accessed via `DebugInfoCollector.Instance` + +--- ## Third-party dependencies -- **DOTween** (`Assets/Scripts/DG/DOTween/`) — used for low-pass filter tweening and cross-fades +- **DOTween** (`Assets/Scripts/DG/DOTween/`) — used for low-pass filter tweening (`DOTween.To`), fade-in/fade-out curves (`AudioSource.DOFade`) - **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 @@ -81,9 +316,34 @@ The mixer hierarchy is hard-referenced by path strings. These must match exactly - `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`) -- Audio configs: `Resources/AudioData/` (loaded via `AudioConfigLoader.Load`) -- Haptic configs: `Resources/HapticData/` -- Haptic clips: `Resources/Haptics/` (`.haptic` files for advanced haptic playback) +All paths are configured via `AudioExtendSettings` at runtime. The defaults are: + +| Resource | Path | Access | +|----------|------|--------| +| Audio clips | `Resources/Audios/` | `ResourceLoader.LoadSync` | +| Audio configs | `Resources/AudioData/` | `AudioConfigLoader.Load` (file-based) or `HapticConfigLoader.Load` (Resources-based) | +| Haptic configs | `Resources/HapticData/` | `HapticConfigLoader.Load` | +| Haptic clips | `Resources/Haptics/` | `.haptic` files for advanced haptic playback | +| AudioMixer | `Resources/Audios/Master` | `Resources.Load` | + +--- + +## Key enums (defined in `HandWrittenDefinitions.cs`) + +| Enum | Values | Purpose | +|------|--------|---------| +| `KillMode` | `Oldest`, `Newest` | Priority preemption strategy | +| `MixingType` | `Sfx`, `Voice`, `Accented` | Output mixer group routing | +| `ContainerType` | `Random`, `Sequence`, `Blend`, `Switch` | Container playback strategy | +| `BlendCrossFadeType` | `Exponential`, `Linear`, `Logarithmic` | (reserved) | +| `AlignMode` | `Immediate`, `Beat`, `Bar` | Music transition timing | +| `CallbackFlags` | `MusicSyncBeat`, `MusicSyncBar`, `MusicSyncGrid` | (reserved) | +| `ActiveSoundState` | `Pending`, `Playing`, `Finished` | Playback lifecycle | +| `SyncPoint` | `Start`, `SameAsCurrentSegment` | Crossfade sync mode | +| `SyncSegment` | `Start`, `LastPlayedSegment` | (reserved) | + +`Parameters.GameState` (generated in `AudioConsts.cs`) is the default game state enum: `Home`, `Game`, `Win`, `Lose`, `TestA`, `TestB`.