Merge branch 'main' into feature/MusicCallback
This commit is contained in:
@@ -9,9 +9,12 @@ namespace OCES.Audio
|
||||
{
|
||||
public const uint GameState = 1;
|
||||
|
||||
public const uint TileMaterial = 2;
|
||||
|
||||
public static void RegisterAllGameState()
|
||||
{
|
||||
StateGroupRegistry.Register<GameState>(1);
|
||||
StateGroupRegistry.Register<TileMaterial>(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,11 @@ namespace OCES.Audio
|
||||
Bass = 6, // 测试用值
|
||||
}
|
||||
|
||||
public enum TileMaterial
|
||||
{
|
||||
Normal, // 普通牌
|
||||
Ice, // 冰
|
||||
Cloud, // 云
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ public partial class AudioObject : IBinarySerializable
|
||||
/// 0 = 随机播放
|
||||
/// 1 = 顺序播放
|
||||
/// 2 = 混合播放
|
||||
/// 3 = 切换播放
|
||||
/// </summary>
|
||||
public ContainerType ContainerType { get; set; }
|
||||
|
||||
@@ -130,6 +131,33 @@ public partial class AudioObject : IBinarySerializable
|
||||
/// </summary>
|
||||
public bool RandomType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Volume Step阈值 ms
|
||||
/// </summary>
|
||||
public uint VolumeStepThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 起始音量
|
||||
/// dB
|
||||
/// </summary>
|
||||
public int Volume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 音量变化幅度
|
||||
/// dB
|
||||
/// </summary>
|
||||
public int VolumeStep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 StateGroup ID
|
||||
/// </summary>
|
||||
public uint SwitchGroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 匹配失败时的备用 AudioObject ID
|
||||
/// </summary>
|
||||
public uint DefaultSwitchId { get; set; }
|
||||
|
||||
|
||||
public void DeSerialize(BinaryReader reader)
|
||||
{
|
||||
@@ -166,6 +194,11 @@ public partial class AudioObject : IBinarySerializable
|
||||
BlendCrossFadeType = (BlendCrossFadeType)reader.ReadByte();
|
||||
LimitRepetition = reader.ReadByte();
|
||||
RandomType = reader.ReadBoolean();
|
||||
VolumeStepThreshold = reader.ReadUInt32();
|
||||
Volume = reader.ReadInt32();
|
||||
VolumeStep = reader.ReadInt32();
|
||||
SwitchGroupId = reader.ReadUInt32();
|
||||
DefaultSwitchId = reader.ReadUInt32();
|
||||
}
|
||||
|
||||
public void Serialize(BinaryWriter writer)
|
||||
@@ -202,6 +235,11 @@ public partial class AudioObject : IBinarySerializable
|
||||
writer.Write((byte)BlendCrossFadeType);
|
||||
writer.Write(LimitRepetition);
|
||||
writer.Write(RandomType);
|
||||
writer.Write(VolumeStepThreshold);
|
||||
writer.Write(Volume);
|
||||
writer.Write(VolumeStep);
|
||||
writer.Write(SwitchGroupId);
|
||||
writer.Write(DefaultSwitchId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,53 +69,40 @@ public static class AudioObjectDefinitions
|
||||
{ "Bar", 52 },
|
||||
{ "Beat", 53 },
|
||||
{ "Grid", 54 },
|
||||
{ "sfx_amb_desert", 2000 },
|
||||
{ "sfx_amb_forest", 2001 },
|
||||
{ "sfx_anim_common_item_fly", 3000 },
|
||||
{ "sfx_anim_corePlay_character_footstep_grass", 3001 },
|
||||
{ "sfx_anim_corePlay_character_footstep_sand", 3002 },
|
||||
{ "sfx_anim_corePlay_character_footstep_stone", 3003 },
|
||||
{ "sfx_anim_corePlay_fireBall", 3004 },
|
||||
{ "sfx_anim_corePlay_freeze", 3005 },
|
||||
{ "sfx_anim_corePlay_getIn_devil", 3006 },
|
||||
{ "sfx_anim_corePlay_getIn_dragon", 3007 },
|
||||
{ "sfx_anim_corePlay_getIn_ghost", 3008 },
|
||||
{ "sfx_anim_corePlay_newBoxFromStorage", 3009 },
|
||||
{ "sfx_anim_corePlay_shield_broke_wood", 3010 },
|
||||
{ "sfx_anim_corePlay_shield_broke_crystal", 3011 },
|
||||
{ "sfx_anim_corePlay_shield_broke_eggRoll", 3012 },
|
||||
{ "sfx_anim_corePlay_shield_show_crystal", 3013 },
|
||||
{ "sfx_anim_corePlay_shield_show_eggRoll", 3014 },
|
||||
{ "sfx_anim_corePlay_shield_show_wood", 3015 },
|
||||
{ "sfx_anim_corePlay_shield_underAttack_wood", 3016 },
|
||||
{ "sfx_anim_corePlay_shield_underAttack_crystal", 3017 },
|
||||
{ "sfx_anim_corePlay_shield_underAttack_eggRoll", 3018 },
|
||||
{ "sfx_anim_corePlay_slow", 3019 },
|
||||
{ "sfx_anim_corePlay_speedUp", 3020 },
|
||||
{ "sfx_anim_corePlay_useProp_", 3021 },
|
||||
{ "sfx_anim_cutScene_in", 3022 },
|
||||
{ "sfx_anim_cutScene_out", 3023 },
|
||||
{ "sfx_notice_common_negative", 4000 },
|
||||
{ "sfx_notice_corePlay_losing", 4001 },
|
||||
{ "sfx_notice_corePlay_restart", 4002 },
|
||||
{ "sfx_notice_corePlay_warning", 4003 },
|
||||
{ "sfx_notice_guide", 4004 },
|
||||
{ "sfx_notice_spinWheel_click", 4005 },
|
||||
{ "sfx_notice_spinWheel_getReward", 4006 },
|
||||
{ "sfx_notice_corePlay_levelStart_hard", 4007 },
|
||||
{ "sfx_ui_labelSwitch_home", 5000 },
|
||||
{ "sfx_ui_panel_common_close", 5001 },
|
||||
{ "sfx_ui_panel_common_open", 5002 },
|
||||
{ "sfx_ui_panel_continue_open", 5003 },
|
||||
{ "sfx_ui_panel_corePlay_guide_open", 5004 },
|
||||
{ "sfx_ui_panel_initPack_open", 5005 },
|
||||
{ "sfx_ui_panel_piggyBank_close", 5006 },
|
||||
{ "sfx_ui_panel_piggyBank_open", 5007 },
|
||||
{ "sfx_ui_panel_removeAds_open", 5008 },
|
||||
{ "0", 5009 },
|
||||
{ "sfx_ui_panel_summerPack_open", 5010 },
|
||||
{ "sfx_ui_panel_unlockItem_open", 5011 },
|
||||
{ "voice_princess_fear", 9000 },
|
||||
{ "NVDice", 55 },
|
||||
{ "NVHeartbeats", 56 },
|
||||
{ "au_sfx_notice_level_countDown_edge", 57 },
|
||||
{ "au_sfx_notice_level_countDown_time", 58 },
|
||||
{ "0,62", 59 },
|
||||
{ "1,65", 59 },
|
||||
{ "2,68", 59 },
|
||||
{ "0,63", 60 },
|
||||
{ "1,66", 60 },
|
||||
{ "2,69", 60 },
|
||||
{ "0,64", 61 },
|
||||
{ "1,67", 61 },
|
||||
{ "2,70", 61 },
|
||||
{ "au_coreplay_choose_v120_a", 62 },
|
||||
{ "au_coreplay_unchoose_v120_a", 63 },
|
||||
{ "au_coreplay_clear_v120_a", 64 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_ice1", 65 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_ice2", 65 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_ice3", 65 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_ice1", 66 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_ice2", 66 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_ice3", 66 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_ice1", 67 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_ice2", 67 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_ice3", 67 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_cloud1", 68 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_cloud2", 68 },
|
||||
{ "au_sfx_ui_button_corePlay_choose_cloud3", 68 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_cloud1", 69 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_cloud2", 69 },
|
||||
{ "au_sfx_ui_button_corePlay_unchoose_cloud3", 69 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_cloud1", 70 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_cloud2", 70 },
|
||||
{ "au_sfx_ui_button_corePlay_clear_cloud3", 70 },
|
||||
};
|
||||
|
||||
public static readonly HashSet<string> AmbiguousNames = new()
|
||||
@@ -143,6 +130,33 @@ public static class AudioObjectDefinitions
|
||||
"Chinese Number 08",
|
||||
"Chinese Number 09",
|
||||
"Chinese Number 10",
|
||||
"0,62",
|
||||
"1,65",
|
||||
"2,68",
|
||||
"0,63",
|
||||
"1,66",
|
||||
"2,69",
|
||||
"0,64",
|
||||
"1,67",
|
||||
"2,70",
|
||||
"au_sfx_ui_button_corePlay_choose_ice1",
|
||||
"au_sfx_ui_button_corePlay_choose_ice2",
|
||||
"au_sfx_ui_button_corePlay_choose_ice3",
|
||||
"au_sfx_ui_button_corePlay_unchoose_ice1",
|
||||
"au_sfx_ui_button_corePlay_unchoose_ice2",
|
||||
"au_sfx_ui_button_corePlay_unchoose_ice3",
|
||||
"au_sfx_ui_button_corePlay_clear_ice1",
|
||||
"au_sfx_ui_button_corePlay_clear_ice2",
|
||||
"au_sfx_ui_button_corePlay_clear_ice3",
|
||||
"au_sfx_ui_button_corePlay_choose_cloud1",
|
||||
"au_sfx_ui_button_corePlay_choose_cloud2",
|
||||
"au_sfx_ui_button_corePlay_choose_cloud3",
|
||||
"au_sfx_ui_button_corePlay_unchoose_cloud1",
|
||||
"au_sfx_ui_button_corePlay_unchoose_cloud2",
|
||||
"au_sfx_ui_button_corePlay_unchoose_cloud3",
|
||||
"au_sfx_ui_button_corePlay_clear_cloud1",
|
||||
"au_sfx_ui_button_corePlay_clear_cloud2",
|
||||
"au_sfx_ui_button_corePlay_clear_cloud3",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
public partial class AudioObjectConfig
|
||||
{
|
||||
Dictionary<uint, Dictionary<int, uint>> m_switchMapping;
|
||||
|
||||
internal void PreParseSwitchMappings()
|
||||
{
|
||||
this.m_switchMapping = new Dictionary<uint, Dictionary<int, uint>>();
|
||||
foreach (AudioObject audioObject in AudioObjectList())
|
||||
{
|
||||
if (audioObject.ContainerType != ContainerType.Switch) continue;
|
||||
this.m_switchMapping[audioObject.Id] = ParseSwitchMapping(audioObject);
|
||||
}
|
||||
}
|
||||
|
||||
public AudioObject GetMappingResult(uint switchContainerId, Enum enumState)
|
||||
{
|
||||
if (!this.m_switchMapping.TryGetValue(switchContainerId, out Dictionary<int, uint> switchMapping))
|
||||
return null;
|
||||
|
||||
return switchMapping.TryGetValue(enumState.GetHashCode(), out uint audioObjectId) ? QueryById(audioObjectId) : null;
|
||||
|
||||
}
|
||||
|
||||
Dictionary<int, uint> ParseSwitchMapping(AudioObject switchContainer)
|
||||
{
|
||||
Dictionary<int, uint> switchMapping = new();
|
||||
foreach (string name in switchContainer.Name)
|
||||
{
|
||||
string[] parts = name.Split(',');
|
||||
if(parts.Length != 2)
|
||||
{
|
||||
Debug.LogWarning($"[AudioSystem] 无法解析 Switch Container {switchContainer.Id} 的映射关系!请检查表");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[0].Trim(), out int stateValue))
|
||||
{
|
||||
Debug.LogWarning($"[AudioSystem] 无法解析 映射关系!请查表");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(parts[1].Trim(), out uint childId))
|
||||
{
|
||||
Debug.LogWarning("");
|
||||
continue;
|
||||
}
|
||||
|
||||
switchMapping.Add(stateValue, childId);
|
||||
}
|
||||
return switchMapping;
|
||||
}
|
||||
|
||||
internal AudioObject GetDefaultSwitchOrFallback(AudioObject switchContainer)
|
||||
{
|
||||
if (switchContainer.DefaultSwitchId == 0)
|
||||
return null;
|
||||
AudioObject defaultChildAudioObject = QueryById(switchContainer.DefaultSwitchId);
|
||||
if (defaultChildAudioObject != null)
|
||||
return defaultChildAudioObject;
|
||||
|
||||
Debug.LogWarning($"[AudioSystem] DefaultSwitch AudioObject {switchContainer.DefaultSwitchId} 不存在。");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68c35969f92d4bb0ba3239d852115c53
|
||||
timeCreated: 1776244015
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using DG.Tweening;
|
||||
@@ -10,7 +10,9 @@ namespace OCES.Audio
|
||||
public class AudioSystem : MonoBehaviour
|
||||
{
|
||||
public static AudioSystem Instance { get; private set; }
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public IReadOnlyDictionary<Type, Enum> ActiveStates { get; private set; }
|
||||
|
||||
const string k_audioConfigPath = "AudioData";
|
||||
const string k_audioResourcePath = "Audios";
|
||||
|
||||
@@ -21,7 +23,7 @@ namespace OCES.Audio
|
||||
AudioGroupConfig m_groups;
|
||||
AudioMixer m_mixer;
|
||||
Tween m_lowpassTween;
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 公开接口
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -42,6 +44,16 @@ namespace OCES.Audio
|
||||
|
||||
public void Play(AudioObject audioObject, Action onPlay = null)
|
||||
{
|
||||
if (audioObject.ContainerType == ContainerType.Switch)
|
||||
{
|
||||
audioObject = ResolveSwitchContainer(audioObject);
|
||||
if (audioObject == null)
|
||||
{
|
||||
Debug.Log("[AudioSystem] 无法解析Switch Container,检查配置表!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.m_sfxSystem.TryPlay(audioObject, onPlay);
|
||||
}
|
||||
|
||||
@@ -49,8 +61,7 @@ namespace OCES.Audio
|
||||
{
|
||||
Play((uint)audioId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Obsolete("Use Play(uint) instead")]
|
||||
public void Play(string audioName)
|
||||
{
|
||||
@@ -114,6 +125,7 @@ namespace OCES.Audio
|
||||
public void SetState<TEnum>(TEnum state) where TEnum : Enum
|
||||
{
|
||||
this.m_musicSystem.OnStateChanged(state);
|
||||
ActiveStates = this.m_musicSystem.ActiveStates;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -139,6 +151,7 @@ namespace OCES.Audio
|
||||
AudioSourcePool sfxPool = new(sfxPoolRoot.transform); // 不传 mixer group,让 SfxSystem 自己设置
|
||||
this.m_sfxSystem = gameObject.AddComponent<SfxSystem>();
|
||||
this.m_audioObjects = AudioConfigLoader.Load<AudioObjectConfig>($"{k_audioConfigPath}/AudioObject");
|
||||
this.m_audioObjects.PreParseSwitchMappings();
|
||||
this.m_groups = AudioConfigLoader.Load<AudioGroupConfig>($"{k_audioConfigPath}/AudioGroup");
|
||||
this.m_sfxSystem.Initialize(this.m_groups, this.m_mixer, sfxPool); // 传入 pool
|
||||
|
||||
@@ -182,19 +195,67 @@ namespace OCES.Audio
|
||||
// ── 注册 StateGroup ──
|
||||
EnumIds.RegisterAllGameState();
|
||||
|
||||
ActiveStates = new Dictionary<Type, Enum>();
|
||||
|
||||
// ── 启动默认音乐与环境音 ──
|
||||
// 触发一次初始状态,让音乐系统从默认状态开始匹配
|
||||
SetState(GameState.Home);
|
||||
//SetState(GameState.Home);
|
||||
}
|
||||
|
||||
AudioObject ResolveSwitchContainer(AudioObject switchContainer)
|
||||
{
|
||||
// 遍历 ActiveStates 找到 TypeId 匹配的枚举类型
|
||||
Enum currentStateValue = null;
|
||||
bool foundGroup = false;
|
||||
foreach (KeyValuePair<Type, Enum> keyValuePair in ActiveStates)
|
||||
{
|
||||
if (StateGroupRegistry.GetTypeId(keyValuePair.Key) != switchContainer.SwitchGroupId) continue;
|
||||
currentStateValue = keyValuePair.Value;
|
||||
foundGroup = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundGroup)
|
||||
{
|
||||
Debug.LogWarning($"[AudioSystem] Switch Container {switchContainer.Id} 找不到 TypeId={switchContainer.SwitchGroupId} 对应的状态组。");
|
||||
return this.m_audioObjects.GetDefaultSwitchOrFallback(switchContainer);
|
||||
}
|
||||
|
||||
// 解析AudioObject对象
|
||||
AudioObject childContainer = this.m_audioObjects.GetMappingResult(switchContainer.Id, currentStateValue);
|
||||
return childContainer ?? this.m_audioObjects.GetDefaultSwitchOrFallback(switchContainer);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static class AudioConfigLoader
|
||||
public static class AudioConfigLoader
|
||||
{
|
||||
public static Dictionary<uint, T> Load<T>(string path, Func<T, uint> keySelector)
|
||||
public static T Load<T>(string configPath, string fileName)
|
||||
where T : IBinarySerializable, new()
|
||||
{
|
||||
string json = System.IO.File.ReadAllText(path);
|
||||
var wrapper = JsonUtility.FromJson<AudioObjectArrayWrapper<T>>(json);
|
||||
return wrapper.AudioObjects.ToDictionary(keySelector);
|
||||
string path = Path.Combine(
|
||||
Application.dataPath, "Resources", configPath, fileName);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Debug.LogError($"[AudioImportTool] 找不到配置文件: {path}");
|
||||
return default;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
T config = new T();
|
||||
using MemoryStream ms = new(File.ReadAllBytes(path));
|
||||
using BinaryReader reader = new(ms);
|
||||
config.DeSerialize(reader);
|
||||
return config;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[AudioImportTool] 配置表反序列化失败 {fileName}: {e.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static T Load<T>(string tableName) where T : IBinarySerializable, new()
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine.UI;
|
||||
|
||||
namespace OCES.Audio.Editor
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
public sealed class DebugInfoCollector : MonoBehaviour
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace OCES.Audio.Editor
|
||||
|
||||
public Dictionary<uint, int> ClipConcurrentCount = new();
|
||||
public List<ActiveSound> ActiveSounds = new();
|
||||
public Dictionary<Type, int> ActiveStates = new();
|
||||
public Dictionary<Type, Enum> ActiveStates = new();
|
||||
readonly StringBuilder m_stringBuilder = new();
|
||||
Text m_textComponent;
|
||||
|
||||
@@ -35,10 +35,9 @@ namespace OCES.Audio.Editor
|
||||
this.m_stringBuilder.Clear();
|
||||
|
||||
this.m_stringBuilder.AppendLine("Current States:");
|
||||
foreach (KeyValuePair<Type, int> activeState in this.ActiveStates)
|
||||
foreach (KeyValuePair<Type, Enum> activeState in this.ActiveStates)
|
||||
{
|
||||
string enumName = Enum.GetName(activeState.Key, activeState.Value) ?? activeState.Value.ToString();
|
||||
this.m_stringBuilder.AppendLine($"{activeState.Key.Name} is {enumName}");
|
||||
this.m_stringBuilder.AppendLine($"{activeState.Key.Name} is {activeState.Value}");
|
||||
}
|
||||
this.m_stringBuilder.AppendLine();
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74dabdb41aa034e7b958b53e70d9ae0d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
{
|
||||
|
||||
public static class AudioImportTool
|
||||
{
|
||||
const string k_audioResourcePath = "Audios";
|
||||
const string k_audioConfigPath = "AudioData";
|
||||
|
||||
static string AudioAbsolutePath
|
||||
{
|
||||
get { return Path.Combine(Application.dataPath, "Resources", k_audioResourcePath); }
|
||||
}
|
||||
static string ConfigAbsolutePath
|
||||
{
|
||||
get { return Path.Combine(Application.dataPath, "Resources", k_audioConfigPath); }
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Audio/Apply Audio Import Settings")]
|
||||
public static void RunFromMenu()
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Apply Audio Import Settings",
|
||||
$"将对 {AudioAbsolutePath} 下所有文件重新应用导入设置,确认继续?",
|
||||
"确认", "取消"))
|
||||
{
|
||||
Run();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the audio import tool from the command line in batch mode.
|
||||
/// This method is designed to be called via Unity's -executeMethod command line argument
|
||||
/// and serves as the entry point for automated audio import workflows.
|
||||
/// It initializes the process and delegates to the main Run method to apply
|
||||
/// audio import settings to all supported audio files in the configured directory.
|
||||
/// </summary>
|
||||
public static void RunCli()
|
||||
{
|
||||
Debug.Log("[AudioImportTool] CLI 模式启动");
|
||||
Run();
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
// 1. 加载配置表
|
||||
var audioObjectConfig = AudioConfigLoader.Load<AudioObjectConfig>(ConfigAbsolutePath, "AudioObject.bytes");
|
||||
var musicSegmentConfig = AudioConfigLoader.Load<MusicSegmentConfig>(ConfigAbsolutePath, "MusicSegment.bytes");
|
||||
if (audioObjectConfig == null || musicSegmentConfig == null)
|
||||
{
|
||||
Debug.LogError("[AudioImportTool] 配置表加载失败,终止");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 扫描文件
|
||||
List<string> supportedExtensions = new() { ".wav", ".ogg" };
|
||||
DirectoryInfo dir = new(AudioAbsolutePath);
|
||||
FileInfo[] files = dir.GetFiles().Where(f => supportedExtensions.Contains(f.Extension.ToLower())).ToArray();
|
||||
int total = files.Length, processed = 0, failed = 0;
|
||||
|
||||
AssetDatabase.StartAssetEditing(); // 批量操作,暂停自动刷新
|
||||
try
|
||||
{
|
||||
foreach (FileInfo file in files)
|
||||
{
|
||||
// 转成 Assets/... 相对路径供 AssetDatabase 使用
|
||||
string assetPath = "Assets" + file.FullName
|
||||
.Replace(Application.dataPath, "")
|
||||
.Replace("\\", "/");
|
||||
|
||||
EditorUtility.DisplayProgressBar(
|
||||
"Audio Import Tool",
|
||||
$"处理: {file.Name} ({processed}/{total})",
|
||||
(float)processed / total);
|
||||
|
||||
bool ok = ProcessFile(assetPath, file.Name, audioObjectConfig, musicSegmentConfig);
|
||||
if (ok) processed++;
|
||||
else failed++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.StopAssetEditing();
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
Debug.Log($"[AudioImportTool] 完成:{processed} 成功,{failed} 失败,共 {total} 个文件");
|
||||
}
|
||||
|
||||
static bool ProcessFile(
|
||||
string assetPath, string fileName,
|
||||
AudioObjectConfig audioObjectConfig, MusicSegmentConfig musicSegmentConfig)
|
||||
{
|
||||
AudioImporter importer = AssetImporter.GetAtPath(assetPath) as AudioImporter;
|
||||
if (!importer)
|
||||
{
|
||||
Debug.LogWarning($"[AudioImportTool] 无法获取 AudioImporter: {assetPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
AudioClip clip = AssetDatabase.LoadAssetAtPath<AudioClip>(assetPath);
|
||||
if (!clip)
|
||||
{
|
||||
Debug.LogWarning($"[AudioImportTool] 无法加载 AudioClip: {assetPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
float duration = clip.length;
|
||||
string fileNameNoExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
AudioCategory category = ClassifyAudio(fileNameNoExt, audioObjectConfig, musicSegmentConfig, out AudioObject audioObject);
|
||||
|
||||
importer.ClearSampleSettingOverride("Android");
|
||||
importer.ClearSampleSettingOverride("iOS");
|
||||
|
||||
AudioImporterSampleSettings settings = importer.defaultSampleSettings;
|
||||
settings.compressionFormat = AudioCompressionFormat.Vorbis;
|
||||
|
||||
settings.sampleRateSetting = AudioSampleRateSetting.OverrideSampleRate;
|
||||
settings.preloadAudioData = audioObject?.Haptic != 0;
|
||||
switch (category)
|
||||
{
|
||||
case AudioCategory.Music:
|
||||
settings.sampleRateOverride = 44100;
|
||||
settings.quality = 0.13f;
|
||||
settings.loadType = duration switch
|
||||
{
|
||||
<= 5 => AudioClipLoadType.DecompressOnLoad,
|
||||
>= 15 => AudioClipLoadType.Streaming,
|
||||
_ => AudioClipLoadType.CompressedInMemory,
|
||||
};
|
||||
break;
|
||||
case AudioCategory.Voice:
|
||||
case AudioCategory.SFX:
|
||||
default:
|
||||
settings.sampleRateOverride = 22050; // 音效2022.3.62f3没有32kHz这一档,要是有的话这一档其实最合适
|
||||
settings.quality = 0.5f;
|
||||
settings.loadType = duration switch
|
||||
{
|
||||
>= 15 => AudioClipLoadType.Streaming,
|
||||
_ => AudioClipLoadType.DecompressOnLoad,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
importer.defaultSampleSettings = settings;
|
||||
importer.forceToMono = false;
|
||||
importer.SaveAndReimport();
|
||||
return true;
|
||||
}
|
||||
|
||||
enum AudioCategory { Music, SFX, Voice }
|
||||
|
||||
static AudioCategory ClassifyAudio(
|
||||
string fileName, // 不含扩展名
|
||||
AudioObjectConfig audioObjectConfig,
|
||||
MusicSegmentConfig musicSegmentConfig,
|
||||
out AudioObject matchedObject)
|
||||
{
|
||||
matchedObject = null;
|
||||
|
||||
// 1. MusicSegment 表命中 → Music
|
||||
MusicSegment segment = musicSegmentConfig.MusicSegmentList()
|
||||
.FirstOrDefault(musicSegment => musicSegment.Name == fileName);
|
||||
if (segment != null)
|
||||
return AudioCategory.Music;
|
||||
|
||||
// 2. AudioObject 表命中 → 按 MixingType
|
||||
AudioObject audioObject = audioObjectConfig.AudioObjectList()
|
||||
.FirstOrDefault(audioObject => audioObject.Name != null && audioObject.Name.Contains(fileName));
|
||||
if (audioObject != null)
|
||||
{
|
||||
matchedObject = audioObject;
|
||||
return audioObject.MixingType == MixingType.Voice
|
||||
? AudioCategory.Voice
|
||||
: AudioCategory.SFX; // SFX + Accent 都走 SFX 分支
|
||||
}
|
||||
|
||||
// 3. 文件名 fallback
|
||||
string lower = fileName.ToLowerInvariant();
|
||||
if (lower.Contains("music") || lower.Contains("bgm"))
|
||||
return AudioCategory.Music;
|
||||
if (lower.Contains("sfx"))
|
||||
return AudioCategory.SFX;
|
||||
if (lower.Contains("voice"))
|
||||
return AudioCategory.Voice;
|
||||
|
||||
// 4. 完全兜底
|
||||
Debug.LogWarning($"[AudioImportTool] 无法分类: {fileName},默认当作 SFX 处理");
|
||||
return AudioCategory.SFX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 114d56912b8874446b70f4c83af327b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -29,6 +29,7 @@ namespace OCES.Audio
|
||||
Random = 0,
|
||||
Sequence,
|
||||
Blend,
|
||||
Switch,
|
||||
}
|
||||
|
||||
public enum BlendCrossFadeType
|
||||
@@ -84,19 +85,10 @@ namespace OCES.Audio
|
||||
|
||||
public partial class MusicPath : IPathEntry { }
|
||||
public partial class AmbiencePath : IPathEntry { }
|
||||
|
||||
public partial class MusicContainerConfig
|
||||
|
||||
public class SwitchEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析拍号字符串(如 "4/4", "3/4"),返回每小节拍数。
|
||||
/// </summary>
|
||||
public static int GetBeatsPerBar(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;
|
||||
}
|
||||
public uint SwitchValue;
|
||||
public uint AudioObjectId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ namespace OCES.Audio
|
||||
/// </summary>
|
||||
public class MusicStateRouter
|
||||
{
|
||||
// key: StateGroup enum Type,value: 当前激活的 enum 整数值
|
||||
readonly Dictionary<Type, int> m_activeStates = new();
|
||||
|
||||
// key: StateGroup enum Type,value: 当前激活的 enum 值
|
||||
readonly Dictionary<Type, Enum> m_activeStates = new();
|
||||
|
||||
|
||||
readonly MusicPathConfig m_musicPaths;
|
||||
readonly AmbiencePathConfig m_ambiencePaths;
|
||||
@@ -18,6 +20,7 @@ namespace OCES.Audio
|
||||
// 上一次匹配到的 PathId,用于 Transition 表的 FromPathId 查询
|
||||
public uint LastMusicPathId { get; private set; }
|
||||
public uint LastAmbiencePathId { get; private set; }
|
||||
internal IReadOnlyDictionary<Type, Enum> ActiveStates { get { return this.m_activeStates; }}
|
||||
|
||||
public MusicStateRouter(MusicPathConfig musicPaths, AmbiencePathConfig ambiencePaths)
|
||||
{
|
||||
@@ -31,8 +34,8 @@ namespace OCES.Audio
|
||||
public void SetState<TEnum>(TEnum state, out uint musicContainerId, out uint ambienceContainerId)
|
||||
where TEnum : Enum
|
||||
{
|
||||
// Dictionary<Type, int> 天然保证同一 StateGroup 只保留最新值,直接覆盖即可
|
||||
this.m_activeStates[typeof(TEnum)] = Convert.ToInt32(state);
|
||||
// Dictionary<Type, Enum> 天然保证同一 StateGroup 只保留最新值,直接覆盖即可
|
||||
this.m_activeStates[typeof(TEnum)] = state;
|
||||
|
||||
musicContainerId = MatchBestPath(this.m_musicPaths.MusicPathList(), out uint musicPathId);
|
||||
ambienceContainerId = MatchBestPath(this.m_ambiencePaths.AmbiencePathList(), out uint ambiencePathId);
|
||||
@@ -99,9 +102,9 @@ namespace OCES.Audio
|
||||
}
|
||||
|
||||
bool conditionMet = false;
|
||||
foreach (KeyValuePair<Type, int> kv in this.m_activeStates)
|
||||
foreach (KeyValuePair<Type, Enum> kv in this.m_activeStates)
|
||||
{
|
||||
if (StateGroupRegistry.GetTypeId(kv.Key) != typeId || kv.Value != stateValue)
|
||||
if (StateGroupRegistry.GetTypeId(kv.Key) != typeId || Convert.ToInt32(kv.Value) != stateValue)
|
||||
continue;
|
||||
conditionMet = true;
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio
|
||||
@@ -20,6 +21,11 @@ namespace OCES.Audio
|
||||
// 记录上一次两个通道各自匹配到的 PathId,用于查 Transition 表
|
||||
uint m_lastMusicPathId;
|
||||
uint m_lastAmbiencePathId;
|
||||
|
||||
internal IReadOnlyDictionary<Type, Enum> ActiveStates
|
||||
{
|
||||
get { return this.m_stateRouter.ActiveStates; }
|
||||
}
|
||||
|
||||
internal void Initialize(
|
||||
MusicSegmentConfig segments,
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OCES.Audio.HandWritten;
|
||||
using OCES.Haptic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
@@ -33,6 +35,7 @@ namespace OCES.Audio
|
||||
AudioSourcePool m_pool;
|
||||
AudioContainerSelector m_containerSelector;
|
||||
PitchStepResolver m_pitchStepResolver;
|
||||
VolumeStepResolver m_volumeStepResolver;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void Update()
|
||||
@@ -74,6 +77,7 @@ namespace OCES.Audio
|
||||
this.m_pool = pool;
|
||||
this.m_containerSelector = new AudioContainerSelector();
|
||||
this.m_pitchStepResolver = new PitchStepResolver();
|
||||
this.m_volumeStepResolver = new VolumeStepResolver();
|
||||
|
||||
AudioMixerGroup[] sfx = Find("Master/Regular/SFX");
|
||||
if (sfx.Length > 0) this.m_sfxGroup = sfx[0];
|
||||
@@ -173,7 +177,8 @@ namespace OCES.Audio
|
||||
|
||||
// 执行播放
|
||||
float pitch = this.m_pitchStepResolver.ResolvePitch(audioObject, now); //算一下用不用变调
|
||||
PlayNewSound(audioObject, pitch);
|
||||
float volume = this.m_volumeStepResolver.ResolveVolume(audioObject, now);
|
||||
PlayNewSound(audioObject, pitch, volume);
|
||||
this.m_lastPlayTime[audioObject.Id] = now;
|
||||
onPlay?.Invoke();
|
||||
}
|
||||
@@ -182,12 +187,13 @@ namespace OCES.Audio
|
||||
// 播放逻辑
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
void PlayNewSound(AudioObject audioObject, float pitch)
|
||||
void PlayNewSound(AudioObject audioObject, float pitch, float volume)
|
||||
{
|
||||
ActiveSound active = new()
|
||||
{
|
||||
AudioObject = audioObject,
|
||||
Pitch = pitch,
|
||||
Volume = volume,
|
||||
State = ActiveSoundState.Pending,
|
||||
StartTime = Time.realtimeSinceStartupAsDouble,
|
||||
};
|
||||
@@ -207,7 +213,7 @@ namespace OCES.Audio
|
||||
/// <summary>
|
||||
/// 连续容器播放协程(Random / Sequence 持续模式)
|
||||
/// </summary>
|
||||
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch)
|
||||
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex)
|
||||
{
|
||||
bool isRandom = audioObject.ContainerType == ContainerType.Random;
|
||||
|
||||
@@ -229,7 +235,7 @@ namespace OCES.Audio
|
||||
limitRepetition);
|
||||
|
||||
// 配置并播放
|
||||
if (!SetupSource(source, audioObject, pitch, index))
|
||||
if (!SetupSource(source, chainActive, index))
|
||||
{
|
||||
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
|
||||
yield break;
|
||||
@@ -255,6 +261,7 @@ namespace OCES.Audio
|
||||
this.m_pool.ReturnToPool(source.gameObject);
|
||||
|
||||
//Debug.Log($"[Container - Continuous] 协程正常结束: {audioObject.Name[0]}");
|
||||
// TryStopHaptic(audioObject.Haptic);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -334,8 +341,9 @@ namespace OCES.Audio
|
||||
return this.m_sfxGroup;
|
||||
}
|
||||
|
||||
bool SetupSource(AudioSource source, AudioObject audioObject, float pitch, int clipIndex = 0)
|
||||
bool SetupSource(AudioSource source, ActiveSound activeSound, int clipIndex = 0)
|
||||
{
|
||||
AudioObject audioObject = activeSound.AudioObject;
|
||||
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}"); // TODO 抽象同一资源加载接口
|
||||
if (!clip)
|
||||
{
|
||||
@@ -347,7 +355,8 @@ namespace OCES.Audio
|
||||
source.loop = audioObject.LoopCount < 0;
|
||||
source.priority = audioObject.Priority;
|
||||
source.outputAudioMixerGroup = GetMixerGroup(audioObject.MixingType);
|
||||
source.pitch = pitch;
|
||||
source.pitch = activeSound.Pitch;
|
||||
source.volume = activeSound.Volume;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -364,6 +373,15 @@ namespace OCES.Audio
|
||||
IncrementClipCount(activeSound.AudioObject.Id);
|
||||
}
|
||||
|
||||
if (activeSound.AudioObject.ContainerType == ContainerType.Blend)
|
||||
{
|
||||
Debug.LogWarning($"[Haptic System] Blend container {activeSound.AudioObject.Id} should not have haptic feedback!");
|
||||
}
|
||||
else
|
||||
{
|
||||
TryStartHaptic(activeSound);
|
||||
}
|
||||
|
||||
activeSound.Coroutine = StartCoroutine(RemoveWhenFinished(activeSound));
|
||||
}
|
||||
|
||||
@@ -371,6 +389,7 @@ namespace OCES.Audio
|
||||
{
|
||||
AudioObject audioObject = active.AudioObject;
|
||||
float pitch = active.Pitch;
|
||||
float volume = active.Volume;
|
||||
|
||||
// =======================
|
||||
// Blend(每个clip一个ActiveSound)
|
||||
@@ -385,10 +404,11 @@ namespace OCES.Audio
|
||||
{
|
||||
AudioObject = audioObject,
|
||||
Pitch = pitch,
|
||||
State = ActiveSoundState.Playing
|
||||
Volume = volume,
|
||||
State = ActiveSoundState.Playing,
|
||||
};
|
||||
|
||||
if (!SetupSource(source, audioObject, pitch, i))
|
||||
if (!SetupSource(source, child, i))
|
||||
{
|
||||
this.m_pool.ReturnToPool(source.gameObject);
|
||||
continue;
|
||||
@@ -424,7 +444,7 @@ namespace OCES.Audio
|
||||
int start = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
|
||||
|
||||
active.Coroutine = StartCoroutine(
|
||||
PlayContainerContinuous(sourceSingle, audioObject, active, start, pitch)
|
||||
PlayContainerContinuous(sourceSingle, audioObject, active, start)
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -437,12 +457,12 @@ namespace OCES.Audio
|
||||
{
|
||||
ContainerType.Random => m_containerSelector.PickShuffleIndex(audioObject),
|
||||
ContainerType.Sequence => m_containerSelector.GetNextSequenceIndex(audioObject),
|
||||
_ => 0
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if (!SetupSource(sourceSingle, audioObject, pitch, index))
|
||||
if (!SetupSource(sourceSingle, active, index))
|
||||
{
|
||||
m_pool.ReturnToPool(sourceSingle.gameObject);
|
||||
this.m_pool.ReturnToPool(sourceSingle.gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -491,7 +511,17 @@ namespace OCES.Audio
|
||||
this.m_pool.ReturnToPool(active.Source.gameObject);
|
||||
}
|
||||
|
||||
static void TryStartHaptic(ActiveSound active)
|
||||
{
|
||||
uint hapticId = active.AudioObject.Haptic;
|
||||
if (hapticId == 0) return;
|
||||
HapticSystem.Instance.Play(hapticId, isDirectCall: false);
|
||||
}
|
||||
|
||||
static void TryStopHaptic(uint hapticId)
|
||||
{
|
||||
HapticSystem.Instance.Stop(hapticId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -506,6 +536,7 @@ namespace OCES.Audio
|
||||
public int CurrentLoopCount;
|
||||
public Coroutine Coroutine;
|
||||
public float Pitch;
|
||||
public float Volume;
|
||||
public ActiveSoundState State;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OCES.Audio.HandWritten
|
||||
{
|
||||
public class VolumeStepResolver
|
||||
{
|
||||
readonly Dictionary<uint, int> m_volumeStepCounts = new();
|
||||
readonly Dictionary<uint, double> m_volumeStepLastTime = new();
|
||||
|
||||
internal float ResolveVolume(AudioObject audioObject, double time)
|
||||
{
|
||||
// 计算一下配置的音量是多少
|
||||
float baseVolume = Mathf.Pow(10,audioObject.Volume / 20f);
|
||||
|
||||
// 优化版。看看表现,要是确实Mathf.Pow造成性能卡点了就用这个。
|
||||
//float baseVolume = audioObject.Volume == 0 ? 1f : Mathf.Pow(10,audioObject.Volume / 20f);
|
||||
|
||||
if (audioObject.VolumeStepThreshold == 0) //没配置VolumeStep
|
||||
{
|
||||
return baseVolume;
|
||||
}
|
||||
|
||||
// 超时了,或者没播过
|
||||
if (!this.m_volumeStepLastTime.TryGetValue(audioObject.Id, out double lastVolumeStepTime)
|
||||
|| time - lastVolumeStepTime > audioObject.VolumeStepThreshold)
|
||||
{
|
||||
this.m_volumeStepCounts[audioObject.Id] = 0;
|
||||
this.m_volumeStepLastTime[audioObject.Id] = time;
|
||||
return baseVolume;
|
||||
}
|
||||
|
||||
//命中了
|
||||
int volumeStepCount = this.m_volumeStepCounts[audioObject.Id];
|
||||
volumeStepCount = this.m_volumeStepCounts[audioObject.Id] = volumeStepCount + 1;
|
||||
this.m_volumeStepLastTime[audioObject.Id] = time;
|
||||
return Mathf.Clamp(Mathf.Pow(10, (audioObject.Volume + audioObject.VolumeStep * volumeStepCount) / 20f), 0f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0059d6fc851c474a97bc05f6385f9a48
|
||||
timeCreated: 1776067889
|
||||
Reference in New Issue
Block a user