first commit

This commit is contained in:
2026-03-20 17:55:53 +08:00
commit 41e38311f0
327 changed files with 11557 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4117dfeac7c454d759d1fc27b1291921
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f5a89839ae0140839eea0e3eb7563ca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* AmbiencePath
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class AmbiencePath : IBinarySerializable
{
/// <summary>
/// 从1开始的int
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 在此定义path。格式为: TypeID1,子状态|TypeID2,子状态…… 无需判断的TypeID无需填写。
/// </summary>
public string Path { get; set; }
/// <summary>
/// 此处填写该Path要播放的ContainerID
/// </summary>
public uint ContainerId { get; set; }
/// <summary>
/// 该条规则的优先级
/// </summary>
public int Priority { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
Path = reader.ReadString();
ContainerId = reader.ReadUInt32();
Priority = reader.ReadInt32();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(Path);
writer.Write(ContainerId);
writer.Write(Priority);
}
}
[Serializable]
public partial class AmbiencePathConfig : IBinarySerializable
{
Dictionary<uint,AmbiencePath> m_ambiencePathInfos = new();
List<AmbiencePath> m_ambiencePathInfoList;
public List<AmbiencePath> AmbiencePathList()
{
this.m_ambiencePathInfoList ??= new List<AmbiencePath>(m_ambiencePathInfos.Values);
return this.m_ambiencePathInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
AmbiencePath tempData = new();
tempData.DeSerialize(reader);
this.m_ambiencePathInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_ambiencePathInfos.Count);
foreach (AmbiencePath ambiencePath in this.m_ambiencePathInfos.Values)
{
ambiencePath.Serialize(writer);
}
}
public AmbiencePath QueryById(uint id)
{
return this.m_ambiencePathInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a691dddb740446e2b3cdf251196e64f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,97 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* AmbienceTransition
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class AmbienceTransition : IBinarySerializable
{
/// <summary>
/// FromPathId x 1000 + ToPathId
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 淡出总时长(s)
/// </summary>
public float FadeOutTime { get; set; }
/// <summary>
/// source的段尾偏移量(s)
/// </summary>
public float FadeOutOffset { get; set; }
/// <summary>
/// 淡入总时长(s)
/// </summary>
public float FadeInTime { get; set; }
/// <summary>
/// Destination段首偏移量(s)
/// </summary>
public float FadeInOffset { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
FadeOutTime = reader.ReadSingle();
FadeOutOffset = reader.ReadSingle();
FadeInTime = reader.ReadSingle();
FadeInOffset = reader.ReadSingle();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(FadeOutTime);
writer.Write(FadeOutOffset);
writer.Write(FadeInTime);
writer.Write(FadeInOffset);
}
}
[Serializable]
public partial class AmbienceTransitionConfig : IBinarySerializable
{
Dictionary<uint,AmbienceTransition> m_ambienceTransitionInfos = new();
List<AmbienceTransition> m_ambienceTransitionInfoList;
public List<AmbienceTransition> AmbienceTransitionList()
{
this.m_ambienceTransitionInfoList ??= new List<AmbienceTransition>(m_ambienceTransitionInfos.Values);
return this.m_ambienceTransitionInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
AmbienceTransition tempData = new();
tempData.DeSerialize(reader);
this.m_ambienceTransitionInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_ambienceTransitionInfos.Count);
foreach (AmbienceTransition ambienceTransition in this.m_ambienceTransitionInfos.Values)
{
ambienceTransition.Serialize(writer);
}
}
public AmbienceTransition QueryById(uint id)
{
return this.m_ambienceTransitionInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ad1fa2b6c6b04deeb200751afe832b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System.IO;
namespace OCES.Audio
{
/// <summary>
/// 替换策略类型
/// </summary>
public enum KillMode : byte
{
Oldest, // 打断最早开始的
Newest, // 打断最新开始的
}
/// <summary>
/// 混音分组
/// </summary>
public enum MixingType : byte
{
Sfx = 0,
Music,
Voice,
}
/// <summary>
/// 声音容器类型
/// </summary>
public enum ContainerType : byte{
Random = 0,
Sequence,
Blend,
}
public enum BlendCrossFadeType : byte
{
Exponential = 0,
Linear,
Logarithmic,
}
public enum AlignMode : byte
{
Immediate,
Beat,
Bar,
}
/// <summary>
/// 游戏状态
/// </summary>
public enum GameState
{
Home,
Game,
Win,
Lose,
Guitar,
Bass,
}
public interface IBinarySerializable
{
void DeSerialize(BinaryReader reader);
void Serialize(BinaryWriter writer);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0e257121504141e794758c8e3832b1d4
timeCreated: 1773130192
@@ -0,0 +1,92 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* AudioGroup
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class AudioGroup : IBinarySerializable
{
/// <summary>
/// 唯一ID
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 分组名
/// </summary>
public string Comment { get; set; }
/// <summary>
/// 组发音数限制
/// </summary>
public ushort GroupThrottleCount { get; set; }
/// <summary>
/// 打断模式
/// 0 = 打断最早
/// 1 = 打断最新
/// </summary>
public KillMode KillMode { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
Comment = reader.ReadString();
GroupThrottleCount = reader.ReadUInt16();
KillMode = (KillMode)reader.ReadByte();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(Comment);
writer.Write(GroupThrottleCount);
writer.Write((byte)KillMode);
}
}
[Serializable]
public partial class AudioGroupConfig : IBinarySerializable
{
Dictionary<uint,AudioGroup> m_audioGroupInfos = new();
List<AudioGroup> m_audioGroupInfoList;
public List<AudioGroup> AudioGroupList()
{
this.m_audioGroupInfoList ??= new List<AudioGroup>(m_audioGroupInfos.Values);
return this.m_audioGroupInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
AudioGroup tempData = new();
tempData.DeSerialize(reader);
this.m_audioGroupInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_audioGroupInfos.Count);
foreach (AudioGroup audioGroup in this.m_audioGroupInfos.Values)
{
audioGroup.Serialize(writer);
}
}
public AudioGroup QueryById(uint id)
{
return this.m_audioGroupInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 02bb5bce772fe40b297d105a1c6908eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,245 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* AudioObject
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class AudioObject : IBinarySerializable
{
/// <summary>
/// 唯一ID
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 文件名
/// </summary>
public List<string> Name { get; set; }
/// <summary>
/// 备注
/// </summary>
public string Comment { get; set; }
/// <summary>
/// 初始延迟
/// 单位秒
/// </summary>
public float InitialDelay { get; set; }
/// <summary>
/// -1 = 无限循环
/// 0 = 不循环
/// >= 1 按次数循环
/// </summary>
public short LoopCount { get; set; }
/// <summary>
/// 0 = SFX
/// 1 = Muisc
/// 2 = Voice
/// </summary>
public MixingType MixingType { get; set; }
/// <summary>
/// 发音分组
/// </summary>
public uint Group { get; set; }
/// <summary>
/// 同时发音数限制
/// </summary>
public ushort ThrottleCount { get; set; }
/// <summary>
/// 最小发音间隔
/// </summary>
public ushort MinInterval { get; set; }
/// <summary>
/// 优先级
/// </summary>
public byte Priority { get; set; }
/// <summary>
/// 打断模式
/// 0 = 打断最早
/// 1 = 打断最新
/// </summary>
public KillMode KillMode { get; set; }
/// <summary>
/// Pitch Step阈值 ms
/// </summary>
public uint PitchStepThreshold { get; set; }
/// <summary>
/// 阶段变化半音数
/// </summary>
public sbyte PitchStep { get; set; }
/// <summary>
/// step几步后就不再继续提升音高
/// </summary>
public byte PitchStepLimit { get; set; }
/// <summary>
/// 相关的触感反馈ID
/// </summary>
public uint Haptic { get; set; }
/// <summary>
/// 0 = 随机播放
/// 1 = 顺序播放
/// 2 = 混合播放
/// </summary>
public ContainerType ContainerType { get; set; }
/// <summary>
/// 0 = 步进
/// 1 = 持续
/// </summary>
public bool ContainerPlayMode { get; set; }
/// <summary>
/// 按段落配置每段响应范围,用|分隔不同段落
/// </summary>
public string BlendRanges { get; set; }
/// <summary>
/// 0 = 指数
/// 1 = 线形
/// 2 = 对数
/// </summary>
public BlendCrossFadeType BlendCrossFadeType { get; set; }
/// <summary>
/// 指定避免重复的次数
/// </summary>
public byte LimitRepetition { get; set; }
/// <summary>
/// 0 = Standard
/// 1 = Shuffle
/// </summary>
public bool RandomType { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
var nameCount = reader.ReadInt32();
if (nameCount > 0)
{
Name = new List<string>();
for (int i = 0; i < nameCount; i++)
{
Name.Add(reader.ReadString());
}
}
else
{
Name = null;
}
Comment = reader.ReadString();
InitialDelay = reader.ReadSingle();
LoopCount = reader.ReadInt16();
MixingType = (MixingType)reader.ReadByte();
Group = reader.ReadUInt32();
ThrottleCount = reader.ReadUInt16();
MinInterval = reader.ReadUInt16();
Priority = reader.ReadByte();
KillMode = (KillMode)reader.ReadByte();
PitchStepThreshold = reader.ReadUInt32();
PitchStep = reader.ReadSByte();
PitchStepLimit = reader.ReadByte();
Haptic = reader.ReadUInt32();
ContainerType = (ContainerType)reader.ReadByte();
ContainerPlayMode = reader.ReadBoolean();
BlendRanges = reader.ReadString();
BlendCrossFadeType = (BlendCrossFadeType)reader.ReadByte();
LimitRepetition = reader.ReadByte();
RandomType = reader.ReadBoolean();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
if (Name == null || Name.Count == 0)
{
writer.Write(0);
}
else
{
writer.Write(Name.Count);
for (int i = 0; i < Name.Count; i++)
{
writer.Write(Name[i]);
}
}
writer.Write(Comment);
writer.Write(InitialDelay);
writer.Write(LoopCount);
writer.Write((byte)MixingType);
writer.Write(Group);
writer.Write(ThrottleCount);
writer.Write(MinInterval);
writer.Write(Priority);
writer.Write((byte)KillMode);
writer.Write(PitchStepThreshold);
writer.Write(PitchStep);
writer.Write(PitchStepLimit);
writer.Write(Haptic);
writer.Write((byte)ContainerType);
writer.Write(ContainerPlayMode);
writer.Write(BlendRanges);
writer.Write((byte)BlendCrossFadeType);
writer.Write(LimitRepetition);
writer.Write(RandomType);
}
}
[Serializable]
public partial class AudioObjectConfig : IBinarySerializable
{
Dictionary<uint,AudioObject> m_audioObjectInfos = new();
List<AudioObject> m_audioObjectInfoList;
public List<AudioObject> AudioObjectList()
{
this.m_audioObjectInfoList ??= new List<AudioObject>(m_audioObjectInfos.Values);
return this.m_audioObjectInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
AudioObject tempData = new();
tempData.DeSerialize(reader);
this.m_audioObjectInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_audioObjectInfos.Count);
foreach (AudioObject audioObject in this.m_audioObjectInfos.Values)
{
audioObject.Serialize(writer);
}
}
public AudioObject QueryById(uint id)
{
return this.m_audioObjectInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46155e8c4d2274e68afd6cace56c6a1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,572 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 文件操作类
/// </summary>
public static class FileManager
{
public static bool CreateDir(string dirPath)
{
if (string.IsNullOrEmpty(dirPath))
return false;
if (Directory.Exists(dirPath))
{
Directory.Delete(dirPath, true);
}
Directory.CreateDirectory(dirPath);
return true;
}
/// <summary>
/// 将数据写入二进制文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="data">继承自IBinarySerialize的数据</param>
public static bool WriteBinaryDataToFile(string filePath, IBinarySerializable data)
{
if (string.IsNullOrEmpty(filePath))
return false;
if (File.Exists(filePath))
{
File.Delete(filePath);
}
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
using (var bw = new BinaryWriter(fileStream))
{
data.Serialize(bw);
bw.Flush();
bw.Close();
}
fileStream.Close();
}
return true;
}
/// <summary>
/// 将数据写入二进制文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="datas">类型(小写)和value的字符串键值对</param>
/// <returns></returns>
public static bool WriteBinaryDatasToFile(string filePath, List<Tuple<string, string>> datas)
{
try
{
if (string.IsNullOrEmpty(filePath))
return false;
if (File.Exists(filePath))
{
File.Delete(filePath);
}
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
using (var bw = new BinaryWriter(fileStream))
{
foreach (var data in datas)
{
if (data.Item1.Equals("int"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToInt32(0));
}
else
{
bw.Write(Convert.ToInt32(data.Item2));
}
}
else if (data.Item1.Equals("uint"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToUInt32(0));
}
else
{
bw.Write(Convert.ToUInt32(data.Item2));
}
}else if (data.Item1.Equals("short"))
{
bw.Write(string.IsNullOrEmpty(data.Item2) ? Convert.ToInt16(0) : Convert.ToInt16(data.Item2));
}
else if (data.Item1.Equals("ushort"))
{
bw.Write(string.IsNullOrEmpty(data.Item2) ? Convert.ToUInt16(0) : Convert.ToUInt16(data.Item2));
}
else if (data.Item1.Equals("sbyte"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToSByte(0));
}
else
{
bw.Write(Convert.ToSByte(data.Item2));
}
}
else if (data.Item1.Equals("byte"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToByte(0));
}
else
{
bw.Write(Convert.ToByte(data.Item2));
}
}
else if (data.Item1.Equals("bool"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToBoolean(false));
}
else
{
bw.Write(Convert.ToBoolean(data.Item2));
}
}
else if (data.Item1.Equals("float"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToSingle(0));
}
else
{
bw.Write(Convert.ToSingle(data.Item2));
}
}
else if (data.Item1.Equals("double"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToDouble(0));
}
else
{
bw.Write(Convert.ToDouble(data.Item2));
}
}
else if (data.Item1.Equals("string"))
{
bw.Write(string.IsNullOrEmpty(data.Item2) ? "" : data.Item2.ToString());
}
else if (data.Item1.Equals("long"))
{
if (string.IsNullOrEmpty(data.Item2))
{
bw.Write(Convert.ToInt64(0));
}
else
{
bw.Write(Convert.ToInt64(data.Item2));
}
}
else if (data.Item1.Equals("vector"))
{
//[1.2,3.4,5.6]
var str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
str = str.Replace("]", "").Replace("[", "");
var numStrs = str.Split(',');
int vectorCount = 3;
bw.Write(vectorCount);
for (int i = 0; i < vectorCount; i++)
{
float v = Convert.ToSingle(numStrs[i]);
bw.Write(v);
}
}
}
else if (data.Item1.Equals("vectorlist")) //List<Vector>类型
{
//[[1.2,3.4,5.6],[2.2,3.4,5.6],[3.2,3.4,5.6]]
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
str = str.Replace("]", "").Replace("[", "");
var numStrs = str.Split(',');
bw.Write(Convert.ToInt32(numStrs.Length / 3));
for (int i = 0; i < numStrs.Length; i++)
{
if (i % 3 == 0)
bw.Write(Convert.ToInt32(3));
bw.Write(Convert.ToSingle(numStrs[i]));
}
}
}
else if (data.Item1.Equals("intlist"))
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToInt32(numStrs[i]));
}
}
}
else if (data.Item1.Equals("floatlist"))
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToSingle(numStrs[i]));
}
}
}
else if (data.Item1.Equals("boollist"))
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToBoolean(numStrs[i]));
}
}
}
else if (data.Item1.Equals("stringlist"))
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(numStrs[i]);
}
}
}
else if (data.Item1.Equals("longlist"))
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToInt64(numStrs[i]));
}
}
}
else if (data.Item1.Contains("list<")) //泛型数组类型
{
string str = data.Item2.ToString();
if (string.IsNullOrEmpty(str))
{
bw.Write(Convert.ToInt32(0));
}
else
{
var numStrs = str.Split(',');
bw.Write(numStrs.Length);
var tempS = data.Item1.Substring(5);
var listType = tempS.Substring(0, tempS.Length - 1);
if (listType.Equals("int"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToInt32(numStrs[i]));
}
}
else if (listType.Equals("uint"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToUInt32(numStrs[i]));
}
}
else if (listType.Equals("short"))
{
foreach (string t in numStrs)
{
bw.Write(Convert.ToInt16(t));
}
}
else if (listType.Equals("ushort"))
{
foreach (string t in numStrs)
{
bw.Write(Convert.ToUInt16(t));
}
}
else if (listType.Equals("sbyte"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToSByte(numStrs[i]));
}
}
else if (listType.Equals("byte"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToByte(numStrs[i]));
}
}
else if (listType.Equals("bool"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToBoolean(numStrs[i]));
}
}
else if (listType.Equals("float"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToSingle(numStrs[i]));
}
}
else if (listType.Equals("long"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToInt64(numStrs[i]));
}
}
else if (listType.Equals("string"))
{
for (int i = 0; i < numStrs.Length; i++)
{
bw.Write(Convert.ToString(numStrs[i]));
}
}
else
{
Debug.LogError("数组类型List<T>,T不是支持的Int,Float,String这三种类型,需要扩展类型");
}
}
}
else
{
Debug.LogError($"写入二进制文件,数据类型{data.Item1}没有适配");
return false;
}
}
bw.Flush();
bw.Close();
}
fileStream.Close();
}
return true;
}
catch (Exception ex)
{
Debug.LogError(ex.ToString());
return false;
}
}
/// <summary>
/// 从内存流中读取二进制
/// </summary>
/// <param name="bytes"></param>
/// <param name="data"></param>
/// <returns></returns>
public static bool ReadBinaryDataFromBytes(byte[] bytes, ref IBinarySerializable data)
{
if (bytes == null)
return false;
using (var memoryStream = new MemoryStream(bytes))
{
using (var br = new BinaryReader(memoryStream))
{
data.DeSerialize(br);
br.Close();
}
memoryStream.Close();
}
return true;
}
/// <summary>
/// 读取二进制文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="data"></param>
/// <returns></returns>
public static bool ReadBinaryDataFromFile(string filePath, ref IBinarySerializable data)
{
if (string.IsNullOrEmpty(filePath))
{
return false;
}
using (var fileStream = new FileStream(filePath, FileMode.Open))
{
using (var br = new BinaryReader(fileStream))
{
data.DeSerialize(br);
br.Close();
}
fileStream.Close();
}
return true;
}
public static bool WriteBytesToFile(string filePath, byte[] data)
{
if (string.IsNullOrEmpty(filePath))
return false;
if (File.Exists(filePath))
{
File.Delete(filePath);
}
var file = new FileInfo(filePath);
using (Stream sw = file.Create())
{
sw.Write(data, 0, data.Length);
sw.Flush();
sw.Close();
}
return true;
}
/// <summary>
/// 将字符串写入文件
/// </summary>
/// <param name="filePath"></param>
/// <param name="context"></param>
/// <returns></returns>
public static bool WriteToFile(string filePath, string context)
{
return WriteToFile(filePath, context, Encoding.Default);
}
public static bool WriteToFile(string filePath, string context, Encoding encoding)
{
if (string.IsNullOrEmpty(filePath))
return false;
if (File.Exists(filePath))
{
File.Delete(filePath);
}
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
var data = encoding.GetBytes(context);
fs.Write(data, 0, data.Length);
fs.Flush();
fs.Close();
}
return true;
}
/// <summary>
/// 按行读取
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string ReadAllByLine(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return string.Empty;
}
StringBuilder sb = new StringBuilder();
using (StreamReader sr = new StreamReader(path, Encoding.Default))
{
string line;
while ((line = sr.ReadLine()) != null)
{
sb.AppendLine(line);
}
sr.Close();
}
return sb.ToString();
}
public static byte[] ReadAllBytes(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return null;
}
return File.ReadAllBytes(path);
}
/// <summary>
/// 修改文件内容
/// </summary>
/// <param name="path"></param>
/// <param name="normalStr"></param>
/// <param name="newStr"></param>
public static void ReplaceContent(string path, string normalStr, string newStr)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return;
}
string strContent = File.ReadAllText(path);
strContent = strContent.Replace(normalStr, newStr);
File.WriteAllText(path, strContent);
}
/// <summary>
/// 批量修改文件内容
/// </summary>
/// <param name="path"></param>
/// <param name="newStr"></param>
/// <param name="normalStrs"></param>
public static void ReplaceContent(string path, string newStr, params string[] normalStrs)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return;
}
string strContent = File.ReadAllText(path);
for (int i = 0; i < normalStrs.Length; i++)
{
strContent = strContent.Replace(normalStrs[i], newStr);
}
File.WriteAllText(path, strContent);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d01f74514b28e47a185705e32b9fa967
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,148 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* MusicContainer
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class MusicContainer : IBinarySerializable
{
/// <summary>
/// 一定不要出现循环
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 1000000以下的是musicId 以上的是ContainerId
/// </summary>
public List<uint> Segments { get; set; }
/// <summary>
/// 0 = 随机播放
/// 1 = 顺序播放
/// 2 = 同时播放
/// </summary>
public ContainerType ContainerType { get; set; }
/// <summary>
/// 0 = 步进
/// 1 = 持续
/// </summary>
public bool ContainerPlayMode { get; set; }
/// <summary>
/// sequence: 间隔时间(s)
/// </summary>
public ushort StrategyParam { get; set; }
/// <summary>
/// -1 = 无限循环
/// 0 = 不循环
/// >= 1 按次数循环
/// </summary>
public short LoopCount { get; set; }
/// <summary>
/// 速度
/// 可以是小数
/// </summary>
public float Bpm { get; set; }
/// <summary>
/// 拍号
/// 采用德国体系定义
/// </summary>
public string TimeSig { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
var segmentsCount = reader.ReadInt32();
if (segmentsCount > 0)
{
Segments = new List<uint>();
for (int i = 0; i < segmentsCount; i++)
{
Segments.Add(reader.ReadUInt32());
}
}
else
{
Segments = null;
}
ContainerType = (ContainerType)reader.ReadByte();
ContainerPlayMode = reader.ReadBoolean();
StrategyParam = reader.ReadUInt16();
LoopCount = reader.ReadInt16();
Bpm = reader.ReadSingle();
TimeSig = reader.ReadString();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
if (Segments == null || Segments.Count == 0)
{
writer.Write(0);
}
else
{
writer.Write(Segments.Count);
for (int i = 0; i < Segments.Count; i++)
{
writer.Write(Segments[i]);
}
}
writer.Write((byte)ContainerType);
writer.Write(ContainerPlayMode);
writer.Write(StrategyParam);
writer.Write(LoopCount);
writer.Write(Bpm);
writer.Write(TimeSig);
}
}
[Serializable]
public partial class MusicContainerConfig : IBinarySerializable
{
Dictionary<uint,MusicContainer> m_musicContainerInfos = new();
List<MusicContainer> m_musicContainerInfoList;
public List<MusicContainer> MusicContainerList()
{
this.m_musicContainerInfoList ??= new List<MusicContainer>(m_musicContainerInfos.Values);
return this.m_musicContainerInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
MusicContainer tempData = new();
tempData.DeSerialize(reader);
this.m_musicContainerInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_musicContainerInfos.Count);
foreach (MusicContainer musicContainer in this.m_musicContainerInfos.Values)
{
musicContainer.Serialize(writer);
}
}
public MusicContainer QueryById(uint id)
{
return this.m_musicContainerInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2999fe859bb9f448aa87bbe6495ce7aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* MusicPath
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class MusicPath : IBinarySerializable
{
/// <summary>
/// 从1开始的int
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 在此定义path。格式为: TypeID1,子状态|TypeID2,子状态…… 无需判断的TypeID无需填写。
/// </summary>
public string Path { get; set; }
/// <summary>
/// 此处填写该Path要播放的ContainerID
/// </summary>
public uint ContainerId { get; set; }
/// <summary>
/// 该条规则的优先级
/// </summary>
public int Priority { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
Path = reader.ReadString();
ContainerId = reader.ReadUInt32();
Priority = reader.ReadInt32();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(Path);
writer.Write(ContainerId);
writer.Write(Priority);
}
}
[Serializable]
public partial class MusicPathConfig : IBinarySerializable
{
Dictionary<uint,MusicPath> m_musicPathInfos = new();
List<MusicPath> m_musicPathInfoList;
public List<MusicPath> MusicPathList()
{
this.m_musicPathInfoList ??= new List<MusicPath>(m_musicPathInfos.Values);
return this.m_musicPathInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
MusicPath tempData = new();
tempData.DeSerialize(reader);
this.m_musicPathInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_musicPathInfos.Count);
foreach (MusicPath musicPath in this.m_musicPathInfos.Values)
{
musicPath.Serialize(writer);
}
}
public MusicPath QueryById(uint id)
{
return this.m_musicPathInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d4ef279e344b4e9eafc0b9a32edf771
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* MusicSegment
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class MusicSegment : IBinarySerializable
{
/// <summary>
/// 唯一ID
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 指向的文件名
/// </summary>
public string Name { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
Name = reader.ReadString();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(Name);
}
}
[Serializable]
public partial class MusicSegmentConfig : IBinarySerializable
{
Dictionary<uint,MusicSegment> m_musicSegmentInfos = new();
List<MusicSegment> m_musicSegmentInfoList;
public List<MusicSegment> MusicSegmentList()
{
this.m_musicSegmentInfoList ??= new List<MusicSegment>(m_musicSegmentInfos.Values);
return this.m_musicSegmentInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
MusicSegment tempData = new();
tempData.DeSerialize(reader);
this.m_musicSegmentInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_musicSegmentInfos.Count);
foreach (MusicSegment musicSegment in this.m_musicSegmentInfos.Values)
{
musicSegment.Serialize(writer);
}
}
public MusicSegment QueryById(uint id)
{
return this.m_musicSegmentInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea0ddf81033174975af33f019e7ae168
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,113 @@
/*
* auto generated by tools(注意:千万不要手动修改本文件)
* MusicTransition
*/
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace OCES.Audio
{
[Serializable]
public partial class MusicTransition : IBinarySerializable
{
/// <summary>
/// FromPathId x 1000 + ToPathId
/// </summary>
public uint Id { get; set; }
/// <summary>
/// 淡出总时长(s)
/// </summary>
public float FadeOutTime { get; set; }
/// <summary>
/// source的段尾偏移量(s)
/// </summary>
public float FadeOutOffset { get; set; }
/// <summary>
/// 淡入总时长(s)
/// </summary>
public float FadeInTime { get; set; }
/// <summary>
/// Destination段首偏移量(s)
/// </summary>
public float FadeInOffset { get; set; }
/// <summary>
/// 0 = 立即切换
/// 1 = 拍
/// 2 = 小节
/// </summary>
public AlignMode AlignMode { get; set; }
/// <summary>
///
/// </summary>
public uint Segment { get; set; }
public void DeSerialize(BinaryReader reader)
{
Id = reader.ReadUInt32();
FadeOutTime = reader.ReadSingle();
FadeOutOffset = reader.ReadSingle();
FadeInTime = reader.ReadSingle();
FadeInOffset = reader.ReadSingle();
AlignMode = (AlignMode)reader.ReadByte();
Segment = reader.ReadUInt32();
}
public void Serialize(BinaryWriter writer)
{
writer.Write(Id);
writer.Write(FadeOutTime);
writer.Write(FadeOutOffset);
writer.Write(FadeInTime);
writer.Write(FadeInOffset);
writer.Write((byte)AlignMode);
writer.Write(Segment);
}
}
[Serializable]
public partial class MusicTransitionConfig : IBinarySerializable
{
Dictionary<uint,MusicTransition> m_musicTransitionInfos = new();
List<MusicTransition> m_musicTransitionInfoList;
public List<MusicTransition> MusicTransitionList()
{
this.m_musicTransitionInfoList ??= new List<MusicTransition>(m_musicTransitionInfos.Values);
return this.m_musicTransitionInfoList;
}
public void DeSerialize(BinaryReader reader)
{
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
MusicTransition tempData = new();
tempData.DeSerialize(reader);
this.m_musicTransitionInfos.Add(tempData.Id, tempData);
}
}
public void Serialize(BinaryWriter writer)
{
writer.Write(this.m_musicTransitionInfos.Count);
foreach (MusicTransition musicTransition in this.m_musicTransitionInfos.Values)
{
musicTransition.Serialize(writer);
}
}
public MusicTransition QueryById(uint id)
{
return this.m_musicTransitionInfos.GetValueOrDefault(id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00cf01c892e674363bb452327ec87e00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c95d201fc6aa4713b89bc0960f6d067b
timeCreated: 1773995606
@@ -0,0 +1,106 @@
using System.Collections;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 环境音通道播放器。
/// 与 MusicChannelPlayer 逻辑相同,但使用 AmbienceTransition 表,不涉及节拍对齐。
/// </summary>
public class AmbienceChannelPlayer
{
readonly AmbienceTransitionConfig m_transitionConfig;
readonly MonoBehaviour m_coroutineHost;
readonly ChannelFader m_fader;
ContainerPlayHandle m_currentHandle;
Coroutine m_transitionCoroutine;
uint m_currentContainerId;
public AmbienceChannelPlayer(
AmbienceTransitionConfig transitionConfig,
MusicContainerPlayer player,
MonoBehaviour coroutineHost)
{
this.m_transitionConfig = transitionConfig;
this.m_coroutineHost = coroutineHost;
this.m_fader = new ChannelFader(player, coroutineHost);
}
// ─────────────────────────────────────────────
// 公开接口
// ─────────────────────────────────────────────
public void SwitchTo(uint newContainerId, uint fromPathId, uint toPathId)
{
if (newContainerId == this.m_currentContainerId && this.m_currentHandle != null)
return;
AmbienceTransition 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));
}
public void Stop()
{
if (this.m_transitionCoroutine != null)
this.m_coroutineHost.StopCoroutine(this.m_transitionCoroutine);
this.m_fader.StopCurrent();
}
// ─────────────────────────────────────────────
// Transition 流程(无节拍对齐)
// ─────────────────────────────────────────────
IEnumerator DoTransition(uint newContainerId, AmbienceTransition transition)
{
ContainerPlayHandle outgoing = this.m_fader.CurrentHandle;
float outgoingVolume = this.m_fader.CurrentVolume;
float fadeOutOffSet = transition?.FadeOutOffset ?? 0f;
float fadeOutTime = transition?.FadeOutTime ?? 0f;
float fadeInOffSet = transition?.FadeInOffset ?? 0f;
float fadeInTime = transition?.FadeInTime ?? 0f;
// ContainerPlayHandle outgoingHandle = this.m_currentHandle;
// float outgoingVolume = this.m_currentVolume;
this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeOutBranch(outgoing, outgoingVolume,fadeOutOffSet, fadeOutTime));
yield return this.m_coroutineHost.StartCoroutine(
this.m_fader.FadeInBranch(newContainerId, fadeInOffSet, fadeInTime));
}
// ─────────────────────────────────────────────
// 工具
// ─────────────────────────────────────────────
AmbienceTransition ResolveTransition(uint fromPathId, uint toPathId)
{
// 优先精确匹配
uint exactId = fromPathId * 1000 + toPathId;
AmbienceTransition exact = this.m_transitionConfig.QueryById(exactId);
if (exact != null) return exact;
// From 为任意
uint fromWildcard = 999u * 1000 + toPathId;
AmbienceTransition fromWild = this.m_transitionConfig.QueryById(fromWildcard);
if (fromWild != null) return fromWild;
// To 为任意
uint toWildcard = fromPathId * 1000 + 999u;
AmbienceTransition toWild = this.m_transitionConfig.QueryById(toWildcard);
if (toWild != null) return toWild;
// 全通配
const uint allWild = 999u * 1000 + 999u;
return this.m_transitionConfig.QueryById(allWild);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4d8f04a88faf499baa9e72098bd1083
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace OCES.Audio
{
public class AudioContainerSelector
{
// TODO: 根据 audioObject.ContainerScope 控制随机历史的共享范围:
// - Global(当前实现):同一 AudioObject 的所有并发实例共享同一份 playHistory 和 limitRepetition 队列,
// 不放回随机在全局层面生效,并发实例之间互相感知已播放记录。
// - PerInstance:每个并发播放实例持有独立的 playHistory 和 limitRepetition 队列,
// 不放回随机仅在该实例内生效,实例之间完全隔离。
// 实现思路:PerInstance 模式下将 HashSet/Queue 作为局部变量传入协程,不再写入这两个 Dictionary。
readonly Dictionary<uint, HashSet<int>> m_randomPlayedHistories = new();
readonly Dictionary<uint, Queue<int>> m_randomRecentQueues = new();
readonly Dictionary<uint, int> m_sequenceNextIndex = new();
/// <summary>
/// Shuffle 随机模式下选取 index(支持 LimitRepetition 队列)
/// </summary>
public int PickShuffleIndex(AudioObject audioObject)
{
int count = audioObject.Name.Count;
// LimitRepetition 队列
int limitRepetition = Mathf.Clamp(audioObject.LimitRepetition, 0, count - 1);
if (!this.m_randomRecentQueues.TryGetValue(audioObject.Id, out Queue<int> recent))
{
recent = new Queue<int>();
this.m_randomRecentQueues[audioObject.Id] = recent;
}
if (!audioObject.RandomType)
{
List<int> candidates = Enumerable.Range(0, count)
.Where(i => !recent.Contains(i))
.ToList();
if (candidates.Count == 0)
candidates = Enumerable.Range(0, count).ToList();
int chosen = candidates[Random.Range(0, candidates.Count)];
if (limitRepetition > 0)
{
recent.Enqueue(chosen);
if (recent.Count > limitRepetition)
recent.Dequeue();
}
return chosen;
}
// 不放回随机
if (!this.m_randomPlayedHistories.TryGetValue(audioObject.Id, out HashSet<int> history))
{
history = new HashSet<int>();
this.m_randomPlayedHistories[audioObject.Id] = history;
}
List<int> available = Enumerable.Range(0, count)
.Where(i => !history.Contains(i) && !recent.Contains(i))
.ToList();
if (available.Count == 0)
{
history.Clear();
available = Enumerable.Range(0, count)
.Where(i => !recent.Contains(i))
.ToList();
if (available.Count == 0)
available = Enumerable.Range(0, count).ToList();
}
int chosenIndex = available[Random.Range(0, available.Count)];
history.Add(chosenIndex);
if (limitRepetition > 0)
{
recent.Enqueue(chosenIndex);
if (recent.Count > limitRepetition)
recent.Dequeue();
}
return chosenIndex;
}
/// <summary>
/// 连续容器的 Random 模式下,带 LimitRepetition 逻辑地选取 index
/// </summary>
public int PickNextRandomIndex(AudioObject audioObject, List<int> remainingPool, Queue<int> recentPlayed, int limitRepetition)
{
if (!this.m_randomPlayedHistories.TryGetValue(audioObject.Id, out HashSet<int> history))
{
history = new HashSet<int>();
this.m_randomPlayedHistories[audioObject.Id] = history;
}
List<int> candidates = remainingPool
.Where(i => !recentPlayed.Contains(i))
.ToList();
if (candidates.Count == 0)
candidates = new List<int>(remainingPool);
int index = candidates[Random.Range(0, candidates.Count)];
if (audioObject.RandomType)
remainingPool.Remove(index);
if (limitRepetition > 0)
recentPlayed.Enqueue(index);
if (recentPlayed.Count > limitRepetition)
recentPlayed.Dequeue();
history.Add(index);
return index;
}
/// <summary>
/// 获取 Sequence 模式下的当前 index 并推进游标
/// </summary>
public int GetNextSequenceIndex(AudioObject audioObject)
{
int current = this.m_sequenceNextIndex.GetValueOrDefault(audioObject.Id, 0);
this.m_sequenceNextIndex[audioObject.Id] = (current + 1) % audioObject.Name.Count;
return current;
}
public void ResetHistory(uint id)
{
if (this.m_randomPlayedHistories.TryGetValue(id, out HashSet<int> history))
{
history.Clear();
}
}
public int GetHistoryCount(uint id)
{
return this.m_randomPlayedHistories.TryGetValue(id, out HashSet<int> history) ? history.Count : 0;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3d3c29064584bf1a0cf1f023d5e3f61
timeCreated: 1773368689
@@ -0,0 +1,387 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Audio;
namespace OCES.Audio
{
/// <summary>
/// 音频调度器
/// </summary>
public class AudioScheduler : MonoBehaviour
{
const int k_maxGlobalConcurrent = 128;
//记录某个 AudioObject 最近一次触发播放的时刻,用于 MinInterval 节流判断,是"上次什么时候播过"。
readonly Dictionary<uint, double> m_lastPlayTime = new();
readonly Dictionary<uint, int> m_clipConcurrentCount = new();
readonly List<ActiveSound> m_activeSounds = new();
AudioGroupConfig m_groupConfig;
AudioMixerGroup m_sfxGroup;
AudioMixerGroup m_musicGroup;
AudioMixerGroup m_voiceGroup;
AudioMixerGroup m_ambienceGroup;
AudioSourcePool m_pool;
AudioContainerSelector m_containerSelector;
PitchStepManager m_pitchStepManager;
int GetClipCount(uint id)
{
return this.m_clipConcurrentCount.GetValueOrDefault(id, 0);
}
void IncrementClipCount(uint id)
{
this.m_clipConcurrentCount[id] = GetClipCount(id) + 1;
//Debug.Log($"{id} count added to {GetClipCount(id)}");
}
void DecrementClipCount(uint id)
{
int cur = GetClipCount(id);
if (cur > 0) this.m_clipConcurrentCount[id] = cur - 1;
//Debug.Log($"{id} clip count minus to {GetClipCount(id)}");
}
// ─────────────────────────────────────────────
// 初始化
// ─────────────────────────────────────────────
public void Initialize(AudioGroupConfig groups, AudioMixer mixer)
{
this.m_groupConfig = groups;
this.m_pool = new AudioSourcePool(transform);
this.m_containerSelector = new AudioContainerSelector();
this.m_pitchStepManager = new PitchStepManager();
AudioMixerGroup[] sfx = Find("Master/SFX");
if (sfx.Length > 0) this.m_sfxGroup = sfx[0];
AudioMixerGroup[] ambience = Find("Master/SFX/Ambience");
if (ambience.Length > 0) this.m_ambienceGroup = ambience[0];
AudioMixerGroup[] voice = Find("Master/Voice");
if (voice.Length > 0) this.m_voiceGroup = voice[0];
return;
AudioMixerGroup[] Find(string path) => mixer.FindMatchingGroups(path);
}
// ─────────────────────────────────────────────
// 节流 & 调度入口
// ─────────────────────────────────────────────
internal void TryPlay(AudioObject audioObject)
{
double now = Time.realtimeSinceStartupAsDouble * 1000.0;
// 第一层:时间节流 单位毫秒
if (this.m_lastPlayTime.TryGetValue(audioObject.Id, out double lastTime) &&
now - lastTime < audioObject.MinInterval)
{
Debug.LogWarning($"[Throttle] {audioObject.Name[0]} 未达到最小间隔,取消播放。");
return;
}
// 第二层:单 AudioObject/Clip 并发控制
if (audioObject.ThrottleCount != 0)
{ // TODO 这里和下面的组控制,每次都会分配新List,可以考虑做成成员变量,每次用完clear
// TODO Linq表达式好用但是GC性能差。如果性能有问题可以考虑换成普通表达
List<ActiveSound> sameObject = this.m_activeSounds
.Where(a => a.AudioObject.Id == audioObject.Id)
.ToList();
if (sameObject.Count >= audioObject.ThrottleCount &&
!TryKill(sameObject, audioObject.KillMode, $"[Object] {audioObject.Name[0]}"))
return;
}
// 第三层:Group 并发控制
AudioGroup groupConfig = this.m_groupConfig.QueryById(audioObject.Group);
if (groupConfig == null)
{
groupConfig = this.m_groupConfig.QueryById(1);
Debug.Log($"未找到{audioObject.Id}对应的组文件,已使用默认配置组1。");
}
List<ActiveSound> sameGroup = this.m_activeSounds
.Where(a => a.AudioObject.Group == audioObject.Group)
.ToList();
if (sameGroup.Count >= groupConfig.GroupThrottleCount)
{
List<ActiveSound> lowerPriority = sameGroup
.Where(a => a.AudioObject.Priority > audioObject.Priority)
.ToList();
List<ActiveSound> killCandidates = lowerPriority.Count > 0 ? lowerPriority : sameGroup;
KillMode killMode = lowerPriority.Count > 0 ? groupConfig.KillMode : audioObject.KillMode;
if (!TryKill(killCandidates, killMode, $"[Group] {audioObject.Name[0]}"))
return;
}
// 第四层:全局并发控制
if (this.m_activeSounds.Count >= k_maxGlobalConcurrent)
{
List<ActiveSound> lowerPriority = this.m_activeSounds
.Where(a => a.AudioObject.Priority > audioObject.Priority)
.ToList();
if (lowerPriority.Count == 0 || !TryKill(lowerPriority, KillMode.Oldest, "[Global]"))
return;
}
// 执行播放
float pitch = this.m_pitchStepManager.ResolvePitch(audioObject, now); //算一下用不用变调
PlayNewSound(audioObject, pitch);
this.m_lastPlayTime[audioObject.Id] = now;
}
// ─────────────────────────────────────────────
// 播放逻辑
// ─────────────────────────────────────────────
void PlayNewSound(AudioObject audioObject, float pitch)
{
// Blend:同时播放所有音轨,无需走后续逻辑
if (audioObject.ContainerType == ContainerType.Blend)
{
for (int i = 0; i < audioObject.Name.Count; i++)
ConfigureSource(this.m_pool.AcquireAudioSource(), audioObject, pitch, clipIndex: i, registerRemove: true);
return;
// TODO BlendRanges, BlendCrossFadeType, BlendReactParam 支持
}
AudioSource source = this.m_pool.AcquireAudioSource();
// 持续播放模式(Continuous
if (audioObject.Name.Count > 1 && audioObject.ContainerPlayMode)
{
ActiveSound chainActive = new()
{
Source = source,
AudioObject = audioObject,
StartTime = Time.realtimeSinceStartupAsDouble,
Pitch = pitch,
};
this.m_activeSounds.Add(chainActive);
IncrementClipCount(audioObject.Id);
int continuousStart = audioObject.ContainerType == ContainerType.Random ? -1 : 0;
chainActive.Coroutine =
StartCoroutine(PlayContainerContinuous(source, audioObject, chainActive, continuousStart, pitch));
return;
}
// 单次播放(步进模式)
int startIndex = audioObject.ContainerType switch
{
ContainerType.Random => this.m_containerSelector.PickShuffleIndex(audioObject),
ContainerType.Sequence => this.m_containerSelector.GetNextSequenceIndex(audioObject),
_ => 0,
};
ConfigureSource(source, audioObject, pitch, startIndex, registerRemove: true);
}
/// <summary>
/// 连续容器播放协程(Random / Sequence 持续模式)
/// </summary>
IEnumerator PlayContainerContinuous(AudioSource source, AudioObject audioObject, ActiveSound chainActive, int startIndex, float pitch)
{
bool isRandom = audioObject.ContainerType == ContainerType.Random;
// 初始化 Random 模式的辅助结构
List<int> remainingPool = isRandom ? new List<int>(Enumerable.Range(0, audioObject.Name.Count)) : null;
Queue<int> recentPlayed = isRandom ? new Queue<int>() : null;
int limitRepetition = isRandom ? Mathf.Clamp(audioObject.LimitRepetition, 0, audioObject.Name.Count - 1) : 0;
int index = startIndex;
while (true)
{
// 选择本轮播放的 index
if (isRandom)
index = this.m_containerSelector.PickNextRandomIndex(
audioObject,
remainingPool,
recentPlayed,
limitRepetition);
// 配置并播放
if (!ConfigureSource(source, audioObject, pitch, index, registerRemove: false))
{
Debug.LogError($"音频文件未找到:{audioObject.Name[index]}");
yield break;
}
yield return new WaitWhile(() => source.isPlaying);
// 判断本轮是否播完
bool roundFinished = isRandom
? remainingPool.Count == 0 || this.m_containerSelector.GetHistoryCount(audioObject.Id) == audioObject.Name.Count
: ++index >= audioObject.Name.Count;
if (roundFinished)
{
DecrementClipCount(audioObject.Id);
this.m_activeSounds.Remove(chainActive);
if (isRandom)
this.m_containerSelector.ResetHistory(audioObject.Id);
this.m_pool.ReturnToPool(source.gameObject);
//Debug.Log($"[Container - Continuous] 协程正常结束: {audioObject.Name[0]}");
yield break;
}
}
}
// ─────────────────────────────────────────────
// 工具方法
// ─────────────────────────────────────────────
/// <summary>
/// 尝试打断一个候选声音。返回 false 表示应取消当前播放(Newest 模式)。
/// </summary>
bool TryKill(List<ActiveSound> candidates, KillMode killMode, string logPrefix)
{
if (killMode == KillMode.Newest)
{
Debug.LogWarning($"{logPrefix} 被 KillNewest 取消播放。");
return false;
}
ActiveSound toKill = SelectOldest(candidates);
if (toKill != null)
{
Debug.LogWarning($"{logPrefix} 打断了 {toKill.AudioObject.Name[0]}。");
StopSound(toKill);
}
return true;
}
/// <summary>
/// 从候选列表中选出开始时间最早的声音
/// </summary>
ActiveSound SelectOldest(List<ActiveSound> candidates)
{
ActiveSound oldest = null;
foreach (ActiveSound s in candidates)
{
if (oldest == null || s.StartTime < oldest.StartTime)
{
oldest = s;
}
}
return oldest;
}
/// <summary>
/// 根据 MixingType 获取对应的 AudioMixerGroup
/// </summary>
AudioMixerGroup GetMixerGroup(MixingType type) => type switch
{
MixingType.Music => this.m_musicGroup,
MixingType.Voice => this.m_voiceGroup,
_ => this.m_sfxGroup,
};
bool ConfigureSource(AudioSource source, AudioObject audioObject, float pitch, int clipIndex = 0, bool registerRemove = true)
{
// TODO 现用现找可能会导致主线程卡死,尤其是低端机。需要配合Decompress配置优化性能。
AudioClip clip = Resources.Load<AudioClip>($"Audios/{audioObject.Name[clipIndex]}");
if (!clip)
{
Debug.LogError($"音频文件未找到:{audioObject.Name[clipIndex]}");
return false;
}
source.clip = clip;
source.loop = audioObject.LoopCount < 0;
source.priority = audioObject.Priority;
source.outputAudioMixerGroup = GetMixerGroup(audioObject.MixingType);
source.pitch = pitch;
source.Play();
if (registerRemove)
{
IncrementClipCount(audioObject.Id);
ActiveSound active = new()
{
Source = source,
AudioObject = audioObject,
StartTime = Time.realtimeSinceStartupAsDouble,
Pitch = pitch,
};
this.m_activeSounds.Add(active);
active.Coroutine = StartCoroutine(RemoveWhenFinished(active));
}
return true;
}
IEnumerator RemoveWhenFinished(ActiveSound active)
{
if (active.AudioObject.LoopCount < 0)
{
//Debug.Log($"RemoveWhenFinished协程已结束,因为播放的是无限循环音效{active.Source.clip.name}");
yield break;
}
do
{
yield return new WaitWhile(() => active.Source.isPlaying);
active.CurrentLoopCount++;
}
while (active.AudioObject.LoopCount > 0
&& active.CurrentLoopCount < active.AudioObject.LoopCount
&& PlayAgain(active));
DecrementClipCount(active.AudioObject.Id);
this.m_activeSounds.Remove(active);
this.m_pool.ReturnToPool(active.Source.gameObject);
}
bool PlayAgain(ActiveSound active)
{
active.Source.Play();
return true;
}
void StopSound(ActiveSound active)
{
if (active.Coroutine != null)
{
StopCoroutine(active.Coroutine);
//Debug.Log($"[StopSound] 协程已终止: {active.AudioObject.Name[0]}");
}
active.Source.Stop();
DecrementClipCount(active.AudioObject.Id);
this.m_activeSounds.Remove(active);
this.m_pool.ReturnToPool(active.Source.gameObject);
}
}
/// <summary>
/// 活跃的声音封装
/// </summary>
public class ActiveSound
{
public AudioSource Source;
public AudioObject AudioObject;
// 记录某个具体播放实例的开始时间,用于 SelectOldest 在多个并发实例中挑出最老的那个来打断,是"这个实例是什么时候开始的"。
public double StartTime;
public int CurrentLoopCount;
public Coroutine Coroutine;
public float Pitch;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ca448d69f123346b0b03c828b398cc89
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace OCES.Audio
{
public class AudioSourcePool
{
readonly Transform m_root;
readonly Queue<GameObject> m_audioSourcePool = new();
int m_counter;
AudioMixerGroup m_mixerGroup;
public AudioSourcePool(Transform root, AudioMixerGroup mixerGroup = null)
{
this.m_root = root;
this.m_mixerGroup = mixerGroup;
}
public AudioSource AcquireAudioSource()
{
GameObject go;
if (this.m_audioSourcePool.Count > 0)
{
go = this.m_audioSourcePool.Dequeue();
go.SetActive(true);
}
else
{
this.m_counter++;
go = new GameObject($"AudioSource{this.m_counter}");
go.transform.SetParent(this.m_root, false);
go.AddComponent<AudioSource>();
}
AudioSource audioSource = go.GetComponent<AudioSource>();
audioSource.outputAudioMixerGroup = this.m_mixerGroup;
return audioSource;
}
public void ReturnToPool(GameObject go)
{
AudioSource audioSource = go.GetComponent<AudioSource>();
audioSource.clip = null;
go.SetActive(false);
this.m_audioSourcePool.Enqueue(go);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb9baa4976eb4d109688f379e5aa9b0f
timeCreated: 1773368680
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Audio;
namespace OCES.Audio
{
public class AudioSystem : MonoBehaviour
{
public static AudioSystem Instance { get; private set; }
const string k_audioConfigPath = "AudioData";
const string k_audioResourcePath = "Audios";
AudioScheduler m_scheduler;
MusicSystem m_musicSystem;
AudioObjectConfig m_audioObjects;
AudioGroupConfig m_groups;
AudioMixer m_mixer;
// ─────────────────────────────────────────────
// 公开接口
// ─────────────────────────────────────────────
public void Play(AudioObject audioObject)
{
this.m_scheduler.TryPlay(audioObject);
}
public void Play(int audioId)
{
AudioObject obj = this.m_audioObjects.QueryById((uint)audioId);
if (obj != null)
this.m_scheduler.TryPlay(obj);
}
public void Play(string audioName)
{
// TODO: 按文件名播放
}
/// <summary>
/// 更新游戏状态,驱动音乐与环境音系统切换。
/// 调用示例:AudioSystem.Instance.SetState(GameState.Game);
/// </summary>
public void SetState<TEnum>(TEnum state) where TEnum : Enum
{
this.m_musicSystem.OnStateChanged(state);
}
// ─────────────────────────────────────────────
// 初始化
// ─────────────────────────────────────────────
void Awake()
{
if ((bool)Instance && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
this.m_mixer = Resources.Load<AudioMixer>("Audios/Master");
// ── SFX 调度器 ──
this.m_scheduler = gameObject.AddComponent<AudioScheduler>();
this.m_audioObjects = AudioConfigLoader.Load<AudioObjectConfig>($"{k_audioConfigPath}/AudioObject");
this.m_groups = AudioConfigLoader.Load<AudioGroupConfig>($"{k_audioConfigPath}/AudioGroup");
this.m_scheduler.Initialize(this.m_groups, this.m_mixer);
// ── 音乐与环境音系统 ──
var segments = AudioConfigLoader.Load<MusicSegmentConfig>($"{k_audioConfigPath}/MusicSegment");
var containers = AudioConfigLoader.Load<MusicContainerConfig>($"{k_audioConfigPath}/MusicContainer");
var musicPaths = AudioConfigLoader.Load<MusicPathConfig>($"{k_audioConfigPath}/MusicPath");
var ambiencePaths = AudioConfigLoader.Load<AmbiencePathConfig>($"{k_audioConfigPath}/AmbiencePath");
var musicTransitions = AudioConfigLoader.Load<MusicTransitionConfig>($"{k_audioConfigPath}/MusicTransition");
var ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig>($"{k_audioConfigPath}/AmbienceTransition");
// MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上
this.m_musicSystem = gameObject.AddComponent<MusicSystem>();
// AudioSourcePool 由 MusicSystem 独占一个子节点,与 SFX pool 隔离
GameObject musicPoolRoot = new("MusicSourcePool");
musicPoolRoot.transform.SetParent(transform, false);
AudioMixerGroup musicGroup = this.m_mixer.FindMatchingGroups("Master/Music")[0];
AudioSourcePool musicPool = new(musicPoolRoot.transform, musicGroup);
GameObject ambiencePoolRoot = new("AmbienceSourcePool");
musicPoolRoot.transform.SetParent(transform, false);
AudioMixerGroup ambienceGroup = this.m_mixer.FindMatchingGroups("Master/SFX/Ambience")[0];
AudioSourcePool ambiencePool = new(ambiencePoolRoot.transform, ambienceGroup);
this.m_musicSystem.Initialize(
segments,
containers,
musicPaths,
ambiencePaths,
musicTransitions,
ambienceTransitions,
musicPool,
ambiencePool);
// ── 注册 StateGroup ──
// 在此处注册所有游戏状态 enum,TypeId 需与策划表中填写的数字一致
// 示例(请根据实际 enum 修改):
// StateGroupRegistry.Register<GameState>(1);
// StateGroupRegistry.Register<Theme>(2);
StateGroupRegistry.Register<GameState>(1);
// ── 启动默认音乐与环境音 ──
// 触发一次初始状态,让音乐系统从默认状态开始匹配
SetState(GameState.Home);
}
}
static class AudioConfigLoader
{
public static Dictionary<uint, T> Load<T>(string path, Func<T, uint> keySelector)
{
string json = System.IO.File.ReadAllText(path);
var wrapper = JsonUtility.FromJson<AudioObjectArrayWrapper<T>>(json);
return wrapper.AudioObjects.ToDictionary(keySelector);
}
public static T Load<T>(string tableName) where T : IBinarySerializable, new()
{
TextAsset bytes = Resources.Load<TextAsset>(tableName);
if (!bytes)
Debug.LogError($"未找到表 {tableName}");
IBinarySerializable data = new T();
bool readOk = FileManager.ReadBinaryDataFromBytes(bytes.bytes, ref data);
if (readOk)
return (T)data;
Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}");
return default;
}
class AudioObjectArrayWrapper<T>
{
public T[] AudioObjects;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5ce1f814dd5d46d48bc33c18ba11c44c
timeCreated: 1772431542
@@ -0,0 +1,151 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 与Transition无关的音量/生命周期管理逻辑
/// </summary>
public class ChannelFader
{
readonly MusicContainerPlayer m_player;
readonly MonoBehaviour m_coroutineHost;
public ContainerPlayHandle CurrentHandle { get; private set; }
public uint CurrentContainerId { get; private set; }
public float CurrentVolume { get; private set; }
public ChannelFader(MusicContainerPlayer player, MonoBehaviour coroutineHost)
{
this.m_player = player;
this.m_coroutineHost = coroutineHost;
}
void StartNew(uint containerId, float startVolume)
{
CurrentContainerId = containerId;
CurrentVolume = startVolume;
CurrentHandle = this.m_player.Play(containerId);
}
public void StopCurrent()
{
StopHandle(CurrentHandle);
CurrentHandle = null;
CurrentContainerId = 0;
}
void StopHandle(ContainerPlayHandle handle)
{
if (handle == null) return;
this.m_player.Stop(handle);
}
/// <summary>
/// 淡出分支:fire-and-forget,由调用方 StartCoroutine
/// </summary>
internal IEnumerator FadeOutBranch(ContainerPlayHandle outgoingHandle, float outgoingVolume, float fadeOutOffset, float fadeOutTime)
{
if (outgoingHandle == null) yield break;
if (fadeOutOffset > 0f)
{
Debug.Log($"Waiting for {fadeOutOffset} to fade out.");
yield return new WaitForSeconds(fadeOutOffset);
}
if (fadeOutTime > 0f )
yield return this.m_coroutineHost.StartCoroutine(
FadeOut(outgoingHandle, outgoingVolume, fadeOutTime));
else
StopHandle(outgoingHandle);
}
/// <summary>
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
/// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。
/// </summary>
internal IEnumerator FadeInBranch(uint newContainerId, float fadeInOffset, float fadeInTime, Action onContainerStarted = null)
{
if (fadeInOffset > 0f)
{
Debug.Log($"Waiting {fadeInOffset} to fade in.");
yield return new WaitForSeconds(fadeInOffset);
}
if (newContainerId == 0)
{
CurrentHandle = null;
CurrentContainerId = 0;
yield break;
}
float startVolume = fadeInTime > 0f ? 0f : 1f;
StartNew(newContainerId, startVolume);
if (fadeInTime > 0f )
{
yield return this.m_coroutineHost.StartCoroutine(
FadeIn(CurrentHandle, fadeInTime));
}
}
IEnumerator FadeOut(ContainerPlayHandle handle, float fromVolume, float duration)
{
Debug.Log($"Fading out in {duration} seconds.");
if (handle == null || handle.Cancelled) yield break;
float elapsed = 0f;
List<AudioSource> sources = new();
handle.CollectActiveSources(sources);
while (elapsed < duration)
{
if (handle.Cancelled) break;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
float vol = Mathf.Lerp(fromVolume, 0f, t);
foreach (AudioSource src in sources)
if (src) src.volume = vol;
yield return null;
}
StopHandle(handle);
}
IEnumerator FadeIn(ContainerPlayHandle handle, float duration)
{
Debug.Log($"Fading In {duration} seconds.");
if (handle == null || handle.Cancelled) yield break;
float elapsed = 0f;
List<AudioSource> sources = new();
while (elapsed < duration)
{
if (handle.Cancelled) yield break;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 每帧重新收集(Blend 模式下新 source 可能中途加入)
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
if (src) src.volume = Mathf.Lerp(0f, 1f, t);
yield return null;
}
// 确保最终音量准确
sources.Clear();
handle.CollectActiveSources(sources);
foreach (AudioSource src in sources)
if (src) src.volume = 1f;
CurrentVolume = 1f;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2794446aae0840ec83fa7c5510ee55bf
timeCreated: 1773992080
@@ -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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f1f58aa9d667d4a379642a33e5fb8d76
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,390 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 负责递归播放一个 MusicContainer 树。
/// 被 MusicChannelPlayer 和 AmbienceChannelPlayer 共同使用。
/// 不持有状态机逻辑,只负责"把这个 Container 播完"。
/// </summary>
public class MusicContainerPlayer
{
readonly MusicContainerConfig m_containerConfig;
readonly MusicSegmentConfig m_segmentConfig;
readonly AudioSourcePool m_pool;
readonly MonoBehaviour m_coroutineHost;
UnityEngine.Audio.AudioMixerGroup m_mixerGroup;
// Sequence Step 模式的全局游标,key = containerId
readonly Dictionary<uint, int> m_sequenceStepIndex = new();
// Random 模式的不放回历史,key = containerId
readonly Dictionary<uint, HashSet<uint>> m_randomHistory = new();
public MusicContainerPlayer(
MusicContainerConfig containerConfig,
MusicSegmentConfig segmentConfig,
AudioSourcePool pool,
MonoBehaviour coroutineHost)
{
this.m_containerConfig = containerConfig;
this.m_segmentConfig = segmentConfig;
this.m_pool = pool;
this.m_coroutineHost = coroutineHost;
}
// ─────────────────────────────────────────────
// 公开入口
// ─────────────────────────────────────────────
/// <summary>
/// 开始播放指定 Container,播放完毕后调用 onFinished。
/// 返回可用于外部停止的句柄。
/// </summary>
public ContainerPlayHandle Play(uint containerId, System.Action onFinished = null)
{
MusicContainer container = this.m_containerConfig.QueryById(containerId);
if (container == null)
{
Debug.LogError($"[MusicContainerPlayer] 找不到 ContainerId: {containerId}");
onFinished?.Invoke();
return null;
}
ContainerPlayHandle handle = new();
handle.TargetVolume = 1f;
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
PlayContainerCoroutine(container, handle, onFinished));
return handle;
}
/// <summary>
/// 停止一个播放句柄(立即停止,不淡出)
/// </summary>
public void Stop(ContainerPlayHandle handle)
{
if (handle == null) return;
handle.Cancelled = true;
foreach (ContainerPlayHandle child in handle.ChildHandles)
{
Stop(child);
}
handle.ChildHandles.Clear();
if (handle.Coroutine != null)
this.m_coroutineHost.StopCoroutine(handle.Coroutine);
foreach (AudioSource src in handle.ActiveSources)
{
ReturnSource(src);
}
handle.ActiveSources.Clear();
}
// ─────────────────────────────────────────────
// 核心协程
// ─────────────────────────────────────────────
IEnumerator PlayContainerCoroutine(
MusicContainer container,
ContainerPlayHandle handle,
System.Action onFinished)
{
int loopsCompleted = 0;
while (true)
{
yield return this.m_coroutineHost.StartCoroutine(
PlayContainerOnce(container, handle.TargetVolume, handle));
if (handle.Cancelled) yield break;
loopsCompleted++;
// -1 = 无限循环,一直重复
if (container.LoopCount == -1)
continue;
// 0 = 不循环,播一次就结束
if (container.LoopCount == 0)
break;
// >= 1,播满次数后结束
if (loopsCompleted >= container.LoopCount)
break;
}
if (!handle.Cancelled)
onFinished?.Invoke();
}
/// <summary>
/// 播放一个 Container 一轮(不含循环逻辑)
/// </summary>
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
{
if (container.Segments == null || container.Segments.Count == 0)
yield break;
switch (container.ContainerType)
{
case ContainerType.Blend:
yield return PlayBlend(container, volumeScale, handle);
break;
case ContainerType.Sequence:
yield return PlaySequence(container, volumeScale, handle);
break;
case ContainerType.Random:
yield return PlayRandom(container, volumeScale, handle);
break;
}
}
// ─────────────────────────────────────────────
// Blend(同时播放)
// ─────────────────────────────────────────────
IEnumerator PlayBlend(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
{
// 同时启动所有子元素,等待全部结束
var childHandles = new List<ContainerPlayHandle>();
bool allDone = false;
int remaining = container.Segments.Count;
foreach (uint segId in container.Segments)
{
if (handle.Cancelled) yield break;
ContainerPlayHandle childHandle = PlayChild(segId, volumeScale, () =>
{
remaining--;
if (remaining <= 0) allDone = true;
});
if (childHandle != null)
{
childHandles.Add(childHandle);
handle.ChildHandles.AddRange(childHandles);
}
}
yield return new WaitUntil(() => allDone || handle.Cancelled);
if (handle.Cancelled)
{
foreach (var ch in childHandles)
Stop(ch);
}
}
// ─────────────────────────────────────────────
// Sequence
// ─────────────────────────────────────────────
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
{
bool isStep = container.ContainerPlayMode;
if (isStep)
{
// Step: 每次只播一个,游标全局推进
int index = GetNextSequenceIndex(container);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle);
}
else
{
// Continuous: 顺序播完所有子元素,算一轮
for (int i = 0; i < container.Segments.Count; i++)
{
if (handle.Cancelled) yield break;
// StrategyParam 作为两段之间的间隔时间(秒)
if (i > 0 && container.StrategyParam > 0)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle);
}
}
}
// ─────────────────────────────────────────────
// Random
// ─────────────────────────────────────────────
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle)
{
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
if (isStep)
{
// Step Random: 随机选一个播放,算一次 loopCount
uint chosen = PickRandomChild(container);
yield return PlayChildAndWait(chosen, volumeScale, handle);
}
else
{
// Continuous Random: 随机不放回地播完所有子元素,算一轮
var remaining = new List<uint>(container.Segments);
while (remaining.Count > 0)
{
if (handle.Cancelled) yield break;
int idx = Random.Range(0, remaining.Count);
uint chosen = remaining[idx];
remaining.RemoveAt(idx);
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(chosen, volumeScale, handle);
}
}
}
// ─────────────────────────────────────────────
// 子元素分发(Segment 或嵌套 Container
// ─────────────────────────────────────────────
/// <summary>
/// 播放一个子元素(segment 或 container),等待其完成后返回。
/// </summary>
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle)
{
bool done = false;
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true);
if (child != null)
parentHandle.ChildHandles.Add(child);
yield return new WaitUntil(() => done || parentHandle.Cancelled);
if (parentHandle.Cancelled && child != null)
Stop(child);
}
/// <summary>
/// 启动一个子元素的播放,不等待,返回句柄。
/// </summary>
ContainerPlayHandle PlayChild(uint id, float volumeScale, System.Action onDone)
{
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone);
}
// ─────────────────────────────────────────────
// Segment 播放
// ─────────────────────────────────────────────
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, System.Action onFinished)
{
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
if (segment == null)
{
Debug.LogError($"[MusicContainerPlayer] 找不到 SegmentId: {segmentId}");
onFinished?.Invoke();
return null;
}
AudioClip clip = Resources.Load<AudioClip>($"Audios/{segment.Name}");
if (!clip)
{
Debug.LogError($"[MusicContainerPlayer] 音频文件未找到: {segment.Name}");
onFinished?.Invoke();
return null;
}
AudioSource source = this.m_pool.AcquireAudioSource();
source.clip = clip;
source.loop = false;
source.volume = volumeScale;
source.Play();
var handle = new ContainerPlayHandle();
handle.ActiveSources.Add(source);
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
WaitSegmentFinish(source, handle, onFinished));
return handle;
}
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, System.Action onFinished)
{
yield return new WaitWhile(() => source.isPlaying && !handle.Cancelled);
source.Stop();
ReturnSource(source);
handle.ActiveSources.Remove(source);
if (!handle.Cancelled)
onFinished?.Invoke();
}
// ─────────────────────────────────────────────
// 工具
// ─────────────────────────────────────────────
void ReturnSource(AudioSource source)
{
source.Stop();
this.m_pool.ReturnToPool(source.gameObject);
}
int GetNextSequenceIndex(MusicContainer container)
{
int current = this.m_sequenceStepIndex.GetValueOrDefault(container.Id, 0);
this.m_sequenceStepIndex[container.Id] = (current + 1) % container.Segments.Count;
return current;
}
uint PickRandomChild(MusicContainer container)
{
if (!this.m_randomHistory.TryGetValue(container.Id, out HashSet<uint> history))
{
history = new HashSet<uint>();
this.m_randomHistory[container.Id] = history;
}
List<uint> available = container.Segments.Where(id => !history.Contains(id)).ToList();
if (available.Count == 0)
{
history.Clear();
available = new List<uint>(container.Segments);
}
uint chosen = available[Random.Range(0, available.Count)];
history.Add(chosen);
return chosen;
}
}
// ─────────────────────────────────────────────
// 播放句柄
// ─────────────────────────────────────────────
/// <summary>
/// 一次 Container 播放的句柄,用于外部停止或淡出时访问正在播放的 AudioSource。
/// </summary>
public class ContainerPlayHandle
{
public Coroutine Coroutine;
public bool Cancelled;
public float TargetVolume = 1f;
public List<AudioSource> ActiveSources = new();
public List<ContainerPlayHandle> ChildHandles = new();
/// <summary>
/// 递归收集所有正在发声的 AudioSource(用于淡出)
/// </summary>
public void CollectActiveSources(List<AudioSource> result)
{
result.AddRange(this.ActiveSources);
foreach (ContainerPlayHandle child in this.ChildHandles)
child.CollectActiveSources(result);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 860a68dfcf02146c2b41351d6a887df9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 维护当前激活的游戏状态,在状态变化时全量匹配 Path 表,返回应当播放的 ContainerId。
/// </summary>
public class MusicStateRouter
{
// key: StateGroup enum Typevalue: 当前激活的 enum 整数值
readonly Dictionary<Type, int> m_activeStates = new();
readonly MusicPathConfig m_musicPaths;
readonly AmbiencePathConfig m_ambiencePaths;
// 上一次匹配到的 PathId,用于 Transition 表的 FromPathId 查询
public uint LastMusicPathId { get; private set; }
public uint LastAmbiencePathId { get; private set; }
public MusicStateRouter(MusicPathConfig musicPaths, AmbiencePathConfig ambiencePaths)
{
this.m_musicPaths = musicPaths;
this.m_ambiencePaths = ambiencePaths;
}
/// <summary>
/// 更新某个 StateGroup 的当前值,并重新全量匹配两张 Path 表。
/// </summary>
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);
musicContainerId = MatchBestPath(this.m_musicPaths.MusicPathList(), out uint musicPathId);
ambienceContainerId = MatchBestPath(this.m_ambiencePaths.AmbiencePathList(), out uint ambiencePathId);
LastMusicPathId = musicPathId;
LastAmbiencePathId = ambiencePathId;
}
// ─────────────────────────────────────────────
// 内部匹配逻辑
// ─────────────────────────────────────────────
/// <summary>
/// 遍历路径列表,找到所有满足当前状态的规则,返回 priority 最小(最高优先级)的那条的 ContainerId。
/// </summary>
uint MatchBestPath<T>(List<T> paths, out uint matchedPathId) where T : IPathEntry
{
IPathEntry best = null;
foreach (T path in paths)
{
if (PathMatches(path.Path) && (best == null || path.Priority < best.Priority))
best = path;
}
if (best != null)
{
matchedPathId = best.Id;
return best.ContainerId;
}
matchedPathId = 0;
return 0;
}
/// <summary>
/// 解析 path 字符串,判断当前激活状态是否满足该条规则。
/// 格式:TypeID1,子状态|TypeID2,子状态……
/// 规则中的每一个条件都必须满足(AND 关系)。
/// </summary>
bool PathMatches(string pathStr)
{
if (string.IsNullOrEmpty(pathStr))
return true; // 空 path 视为无条件匹配(默认兜底规则)
string[] conditions = pathStr.Split('|');
foreach (string condition in conditions)
{
string[] parts = condition.Split(',');
if (parts.Length != 2)
{
Debug.LogWarning($"[MusicStateRouter] Path格式错误: '{condition}'");
return false;
}
if (!int.TryParse(parts[0].Trim(), out int typeId) ||
!int.TryParse(parts[1].Trim(), out int stateValue))
{
Debug.LogWarning($"[MusicStateRouter] Path解析失败: '{condition}'");
return false;
}
bool conditionMet = false;
foreach (KeyValuePair<Type, int> kv in this.m_activeStates)
{
if (StateGroupRegistry.GetTypeId(kv.Key) != typeId || kv.Value != stateValue)
continue;
conditionMet = true;
break;
}
if (!conditionMet)
return false;
}
return true;
}
}
// ─────────────────────────────────────────────
// StateGroup 注册表:将 enum Type 映射为策划表中填写的 TypeId 整数
// ─────────────────────────────────────────────
/// <summary>
/// 程序员在此注册所有 StateGroup enum 与其对应 TypeId 的映射关系。
/// 策划在 MusicPath.path 字段中填写的 TypeId 数字必须与此处一致。
/// </summary>
public static class StateGroupRegistry
{
static readonly Dictionary<Type, int> s_typeIdMap = new();
public static void Register<TEnum>(int typeId) where TEnum : Enum
{
s_typeIdMap[typeof(TEnum)] = typeId;
}
public static int GetTypeId(Type enumType)
{
if (s_typeIdMap.TryGetValue(enumType, out int id))
return id;
Debug.LogWarning($"[StateGroupRegistry] 未注册的StateGroup类型: {enumType.Name},请调用StateGroupRegistry.Register<T>()");
return -1;
}
}
// ─────────────────────────────────────────────
// 辅助接口,让泛型方法同时处理 MusicPath 和 AmbiencePath
// ─────────────────────────────────────────────
public interface IPathEntry
{
uint Id { get; }
string Path { get; }
uint ContainerId { get; }
int Priority { get; }
}
public partial class MusicPath : IPathEntry { }
public partial class AmbiencePath : IPathEntry { }
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c82693d4d73e64daf9bbd6854a818dc8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
using System;
using UnityEngine;
namespace OCES.Audio
{
/// <summary>
/// 音乐与环境音系统。由 AudioSystem 持有并初始化。
/// 对外只暴露 OnStateChanged,由 AudioSystem.SetState 转发调用。
/// </summary>
public class MusicSystem : MonoBehaviour
{
MusicStateRouter m_stateRouter;
MusicChannelPlayer m_musicChannel;
AmbienceChannelPlayer m_ambienceChannel;
// 记录上一次两个通道各自匹配到的 PathId,用于查 Transition 表
uint m_lastMusicPathId;
uint m_lastAmbiencePathId;
public void Initialize(
MusicSegmentConfig segments,
MusicContainerConfig containers,
MusicPathConfig musicPaths,
AmbiencePathConfig ambiencePaths,
MusicTransitionConfig musicTransitions,
AmbienceTransitionConfig ambienceTransitions,
AudioSourcePool musicPool,
AudioSourcePool ambiencePool)
{
MusicContainerPlayer musicContainerPlayer = new(containers, segments, musicPool, this);
MusicContainerPlayer ambientContainerPlayer = new(containers, segments, ambiencePool, this);
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
this.m_musicChannel = new MusicChannelPlayer(containers, musicTransitions, musicContainerPlayer, this);
this.m_ambienceChannel = new AmbienceChannelPlayer(ambienceTransitions, ambientContainerPlayer, this);
}
/// <summary>
/// 由 AudioSystem.SetState 调用,更新状态并驱动两个通道切换。
/// </summary>
public void OnStateChanged<TEnum>(TEnum state) where TEnum : Enum
{
this.m_stateRouter.SetState(
state,
out uint musicContainerId,
out uint ambienceContainerId);
uint newMusicPathId = this.m_stateRouter.LastMusicPathId;
uint newAmbiencePathId = this.m_stateRouter.LastAmbiencePathId;
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 936ca15cb5c944ca99e98fece679861e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using UnityEngine;
namespace OCES.Audio
{
public class PitchStepManager
{
readonly Dictionary<uint, int> m_pitchStepCounts = new();
readonly Dictionary<uint, double> m_pitchStepLastTime = new();
internal float ResolvePitch(AudioObject audioObject, double time)
{
if (audioObject.PitchStep == 0 || audioObject.PitchStepThreshold == 0) //没配置PitchStep
{
return 1f;
}
// 超时了,或者没播过
if (!this.m_pitchStepLastTime.TryGetValue(audioObject.Id, out double lastPitchStepTime)
|| time - lastPitchStepTime > audioObject.PitchStepThreshold)
{
this.m_pitchStepCounts[audioObject.Id] = 0;
this.m_pitchStepLastTime[audioObject.Id] = time;
return 1f;
}
//命中了
int pitchStepCount = this.m_pitchStepCounts[audioObject.Id];
pitchStepCount = this.m_pitchStepCounts[audioObject.Id] = Mathf.Min(pitchStepCount + 1, audioObject.PitchStepLimit);
this.m_pitchStepLastTime[audioObject.Id] = time;
return Mathf.Pow(2, audioObject.PitchStep * pitchStepCount / 12f);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: df0d032c5bdd48ee8f7575ca11b3667f
timeCreated: 1773749175
+30
View File
@@ -0,0 +1,30 @@
using UnityEngine;
using UnityEngine.UI;
using OCES.Audio;
namespace OCES
{
public class ButtonInvoker : MonoBehaviour
{
public GameState targetGameState;
Button m_button;
void Awake()
{
this.m_button = GetComponent<Button>();
this.m_button.onClick.AddListener(ButtonPressed);
}
void OnDestroy()
{
this.m_button.onClick.RemoveListener(ButtonPressed);
}
private void ButtonPressed()
{
AudioSystem.Instance.SetState(this.targetGameState);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2ce47fe7df364a8fa37501256e5b5155
timeCreated: 1773901362