WIP: StartOffset

This commit is contained in:
2026-05-07 12:09:16 +08:00
parent 8ea862b546
commit ab6e9e74e0
51 changed files with 810 additions and 64 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 0ff2a59f9d4f5412792e5bba783f5c7a
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 6b648c023f5e041a893c06ee0cd55839
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: deba7646359d5433aaecc6dbf9ec8e36
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: cd9d756f436864f1c8b6a10dc273c708
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 207d0e7be17864f8996e1f3d2140d117
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: a91860e6b5a5c47d9a8acaac55bebf40
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: d8d6d1c358a444de6bf2863ee12c6a6e
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: f805529718cf2490c81ddeeeca559a6c
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 0546af815ebd141fabcda647c12e8ccf
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 2
sampleRateSetting: 2
sampleRateOverride: 44100
compressionFormat: 1
quality: 0.13
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 921d30b7911b6459b82aeb4ce80bcae9
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4e8fa10885f1b4a81a45db662ff7af77
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: ae19d4b3ec8be4679b092fbefc78f066
AudioImporter:
externalObjects: {}
serializedVersion: 7
defaultSettings:
serializedVersion: 2
loadType: 2
sampleRateSetting: 2
sampleRateOverride: 44100
compressionFormat: 1
quality: 0.13
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,73 @@
{
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"metadata": {
"editor": "Haptic Editor",
"author": "Oliver",
"source": "au_coreplay_collect_handbag",
"project": "Magic_Sort",
"tags": [],
"description": ""
},
"signals": {
"continuous": {
"envelopes": {
"amplitude": [
{
"time": 0.42857142857142855,
"amplitude": 0,
"emphasis": {
"amplitude": 0.25,
"frequency": 0.5
}
},
{
"time": 0.4612427930813581,
"amplitude": 0,
"emphasis": {
"amplitude": 0.5,
"frequency": 0.5
}
},
{
"time": 0.4855861627162076,
"amplitude": 0,
"emphasis": {
"amplitude": 0.75,
"frequency": 0.5
}
},
{
"time": 0.5393978219090326,
"amplitude": 0,
"emphasis": {
"amplitude": 1,
"frequency": 0.5
}
}
],
"frequency": [
{
"time": 0.42857142857142855,
"frequency": 0
},
{
"time": 0.4612427930813581,
"frequency": 0
},
{
"time": 0.4855861627162076,
"frequency": 0
},
{
"time": 0.5393978219090326,
"frequency": 0
}
]
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f26a3f5c6d14a44c196a2c50e54f37ec
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: dc84fb4fa9e67485a972c887d976d004, type: 3}
@@ -0,0 +1,41 @@
{
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"metadata": {
"editor": "Haptic Editor",
"author": "Oliver",
"source": "new_au_coreplay_pour_less_1",
"project": "Magic_Sort",
"tags": [],
"description": ""
},
"signals": {
"continuous": {
"envelopes": {
"amplitude": [
{
"time": 0,
"amplitude": 0
},
{
"time": 0.4426649583600256,
"amplitude": 0.15
}
],
"frequency": [
{
"time": 0,
"frequency": 0.0810719131614654
},
{
"time": 0.4426649583600256,
"frequency": 0.25
}
]
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ee687f540e1b84654a3ab68e821cd838
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: dc84fb4fa9e67485a972c887d976d004, type: 3}
@@ -0,0 +1,41 @@
{
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"metadata": {
"editor": "Haptic Editor",
"author": "Oliver",
"source": "au_coreplay_pour_mid_1.wav",
"project": "Magic_Sort",
"tags": [],
"description": ""
},
"signals": {
"continuous": {
"envelopes": {
"amplitude": [
{
"time": 0,
"amplitude": 0
},
{
"time": 0.513,
"amplitude": 0.4
}
],
"frequency": [
{
"time": 0,
"frequency": 0
},
{
"time": 0.513,
"frequency": 0.4
}
]
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: cc4f61e064f1440e3adc42cb52023fb9
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: dc84fb4fa9e67485a972c887d976d004, type: 3}
@@ -0,0 +1,41 @@
{
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"metadata": {
"editor": "Haptic Editor",
"author": "Oliver",
"source": "au_coreplay_pour_more_1.wav",
"project": "Magic_Sort",
"tags": [],
"description": ""
},
"signals": {
"continuous": {
"envelopes": {
"amplitude": [
{
"time": 0,
"amplitude": 0
},
{
"time": 0.7,
"amplitude": 0.4
}
],
"frequency": [
{
"time": 0,
"frequency": 0
},
{
"time": 0.7,
"frequency": 0.5
}
]
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a5e848c536ee74d9a9d7c1e782ffc106
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: dc84fb4fa9e67485a972c887d976d004, type: 3}
@@ -0,0 +1,73 @@
{
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"metadata": {
"editor": "Haptic Editor",
"author": "Oliver",
"source": "au_coreplay_win.wav",
"project": "Magic_Sort",
"tags": [],
"description": ""
},
"signals": {
"continuous": {
"envelopes": {
"amplitude": [
{
"time": 0.0006406149903907751,
"amplitude": 0
},
{
"time": 0.05509288917360666,
"amplitude": 0.73,
"emphasis": {
"amplitude": 0.75,
"frequency": 0.25
}
},
{
"time": 0.1646380525304292,
"amplitude": 0.26424694708276797
},
{
"time": 0.24407431133888532,
"amplitude": 1,
"emphasis": {
"amplitude": 1,
"frequency": 0.39
}
},
{
"time": 0.7021140294682896,
"amplitude": 0
}
],
"frequency": [
{
"time": 0.0006406149903907751,
"frequency": 0
},
{
"time": 0.05509288917360666,
"frequency": 0.5
},
{
"time": 0.1646380525304292,
"frequency": 0.2472862957937585
},
{
"time": 0.24407431133888532,
"frequency": 0.7459294436906377
},
{
"time": 0.7021140294682896,
"frequency": 0
}
]
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 2dc504ef835db46b89c366c2b0650175
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: dc84fb4fa9e67485a972c887d976d004, type: 3}
+14 -14
View File
@@ -520,6 +520,7 @@ GameObject:
- component: {fileID: 519420031}
- component: {fileID: 519420029}
- component: {fileID: 519420030}
- component: {fileID: 519420033}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -645,6 +646,18 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &519420033
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a3d8ad76f12545caa287f26889c674e3, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &519563560
GameObject:
m_ObjectHideFlags: 0
@@ -1948,7 +1961,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 2ce47fe7df364a8fa37501256e5b5155, type: 3}
m_Name:
m_EditorClassIdentifier:
targetGameState: 2
targetGameState: 4
enableLowpass: 0
buttonText: {fileID: 519563562}
--- !u!114 &1542973985
@@ -2817,7 +2830,6 @@ GameObject:
m_Component:
- component: {fileID: 2093584671}
- component: {fileID: 2093584670}
- component: {fileID: 2093584672}
m_Layer: 0
m_Name: AudioSystem
m_TagString: Untagged
@@ -2854,18 +2866,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2093584672
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2093584669}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a3d8ad76f12545caa287f26889c674e3, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -36,6 +36,11 @@ namespace OCES.Audio
public static uint Play_Bar = 52;
public static uint Play_Beat = 53;
public static uint Play_Grid = 54;
public static uint Play_au_coreplay_collect_handbag = 101;
public static uint Play_au_coreplay_win = 102;
public static uint Play_au_coreplay_pour_less = 103;
public static uint Play_au_coreplay_pour_mid = 104;
public static uint Play_au_coreplay_pour_more = 105;
} //public class Cues
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
@@ -68,6 +73,14 @@ namespace OCES.Audio
{ "Bar", 52 },
{ "Beat", 53 },
{ "Grid", 54 },
{ "au_coreplay_collect_handbag", 101 },
{ "au_coreplay_win", 102 },
{ "au_coreplay_pour_less_1", 103 },
{ "au_coreplay_pour_less_2", 103 },
{ "au_coreplay_pour_mid_1", 104 },
{ "au_coreplay_pour_mid_2", 104 },
{ "au_coreplay_pour_more_1", 105 },
{ "au_coreplay_pour_more_2", 105 },
};
public static readonly HashSet<string> AmbiguousNames = new()
@@ -76,6 +89,12 @@ namespace OCES.Audio
public static readonly HashSet<string> SharedIdNames = new()
{
"au_coreplay_pour_less_1",
"au_coreplay_pour_less_2",
"au_coreplay_pour_mid_1",
"au_coreplay_pour_mid_2",
"au_coreplay_pour_more_1",
"au_coreplay_pour_more_2",
};
} //public class NameDictionaries
@@ -99,6 +118,7 @@ namespace OCES.Audio
Game, // 关卡内
Win, // 胜利
Lose, // 失败
Test,
}
} //public class Parameters
@@ -11,6 +11,7 @@ namespace OCES.Audio
double m_startDspTime, m_secondsPerBeat; // 本次计数周期的起点
Coroutine m_beatCoroutine, m_barCoroutine, m_gridCoroutine;
Coroutine m_delayCoroutine;
readonly Action<uint> m_onBeat, m_onBar, m_onGrid;
readonly MonoBehaviour m_host;
@@ -26,13 +27,13 @@ namespace OCES.Audio
this.m_onGrid = onGrid;
}
internal void Restart(MusicContainer container, float inheritedBpm, double dspTime)
internal void Restart(MusicContainer container, float inheritedBpm, double dspTime, double startOffset = 0d)
{
//Debug.Log($"[BeatClock] Restarting {container.Id}, inheritedBpm = {inheritedBpm}, dspTime = {dspTime}");
StopAll();
this.m_blendError = this.m_stopped = false;
this.m_containerId = container.Id;
this.m_startDspTime = dspTime;
this.m_startDspTime = dspTime + startOffset;
// BPM:优先用自己的,为 0 则用传入的继承值
float bpm = container.Bpm > 0f ? container.Bpm : inheritedBpm;
@@ -57,6 +58,32 @@ namespace OCES.Audio
this.m_barsPerGrid = container.Grid;
}
double delay = this.m_startDspTime - AudioSettings.dspTime;
if (delay > 0)
{
this.m_delayCoroutine = this.m_host.StartCoroutine(DelayedStart(delay, hasTimeSig, hasGrid));
}
else
{
StartCoroutines(hasTimeSig, hasGrid);
}
}
IEnumerator DelayedStart(double delay, bool hasTimeSig, bool hasGrid)
{
yield return new WaitUntil(() => AudioSettings.dspTime >= this.m_startDspTime - 0.02);
while (AudioSettings.dspTime < this.m_startDspTime)
yield return null;
if (this.m_stopped || this.m_blendError) yield break;
this.m_delayCoroutine = null;
StartCoroutines(hasTimeSig, hasGrid);
}
void StartCoroutines(bool hasTimeSig, bool hasGrid)
{
// 分层启动协程
// Beat:只要 BPM 有效就启动
this.m_beatCoroutine = this.m_host.StartCoroutine(BeatCoroutine());
@@ -82,10 +109,11 @@ namespace OCES.Audio
internal void StopAll()
{
this.m_stopped = true;
if (this.m_delayCoroutine != null) this.m_host.StopCoroutine(this.m_delayCoroutine);
if (this.m_beatCoroutine != null) this.m_host.StopCoroutine(this.m_beatCoroutine);
if (this.m_barCoroutine != null) this.m_host.StopCoroutine(this.m_barCoroutine);
if (this.m_gridCoroutine != null) this.m_host.StopCoroutine(this.m_gridCoroutine);
this.m_beatCoroutine = this.m_barCoroutine = this.m_gridCoroutine = null;
this.m_delayCoroutine = this.m_beatCoroutine = this.m_barCoroutine = this.m_gridCoroutine = null;
}
internal double GetNextDspTime(AlignMode mode)
@@ -98,6 +126,11 @@ namespace OCES.Audio
}
double elapsed = now - this.m_startDspTime;
if (elapsed < 0)
{
return this.m_startDspTime;
}
double period = mode switch
{
AlignMode.Beat => this.m_secondsPerBeat,
@@ -16,6 +16,9 @@ namespace OCES.Audio
public double BaseDspTime; // 读取Source Segment 的 dspTime
public int SampleRate; // Source Segment 的采样率
public bool Ready;
public double SourceStartOffset;
public double TargetAudioTime;
public double StartPlayTime;
}
/// <summary>
@@ -41,10 +44,12 @@ namespace OCES.Audio
CurrentContainerId = containerId;
CurrentVolume = startVolume;
CurrentHandle = this.m_player.Play(containerId);
Debug.Log($"[ChannelFader] StartNew: containerId={containerId}, CurrentHandle={CurrentHandle}");
}
public void StopCurrent()
{
Debug.Log($"[ChannelFader] StopCurrent called! CurrentHandle={CurrentHandle}, stack=\n{Environment.StackTrace}");
StopHandle(CurrentHandle);
CurrentHandle = null;
CurrentContainerId = 0;
@@ -84,7 +89,8 @@ namespace OCES.Audio
/// 淡入分支:等待 FadeInOffset 后启动新音乐并淡入。
/// 主协程 yield return 此分支,以便 DoTransition 在新音乐就绪后才结束。
/// </summary>
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, SyncPointState syncState = null, Action onContainerStarted = null)
internal IEnumerator FadeInBranch(uint newContainerId, ITransitionConfig transition, SyncPointState syncState = null,
Action onContainerStarted = null)
{
if (transition?.FadeInOffset > 0f)
{
@@ -102,25 +108,29 @@ namespace OCES.Audio
float startVolume = transition?.FadeInTime > 0f ? 0f : 1f;
StartNew(newContainerId, startVolume);
onContainerStarted?.Invoke();
switch (syncState)
{
// SyncPoint: 获取新 Segment 的 AudioSource 并设置 timeSamples
if (syncState is { Mode: SyncPoint.SameAsCurrentSegment })
case { Mode: SyncPoint.SameAsCurrentSegment }:
{
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (newSource && newSource.clip)
{
// 计算从读取Source Segment 到现在经过的 samples
double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime;
int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate);
int targetTimeSamples = syncState.BaseTimeSamples + elapsedSamples;
if (targetTimeSamples < newSource.clip.samples)
double targetAudioTime = syncState.TargetAudioTime;
if (targetAudioTime > 0)
{
int targetTimeSamples = (int)(targetAudioTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
else
{
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 新音频 samples({newSource.clip.samples}) <= 目标 samples({targetTimeSamples}),降级为 Start");
Debug.LogError(
$"[ChannelFader] SyncPoint: 目标 samples({targetTimeSamples}) 超出范围 [0, {newSource.clip.samples}),降级为 Start");
}
}
}
else
@@ -128,9 +138,25 @@ namespace OCES.Audio
Debug.LogError($"[ChannelFader] SyncPoint.SameAsCurrentSegment: 未能获取新 Segment 的 AudioSource,降级为 Start");
}
syncState.Ready = true;
break;
}
case { Mode: SyncPoint.Start, StartPlayTime: > 0 }:
{
AudioSource newSource = CurrentHandle?.GetFirstLeafSource();
if (!newSource || !newSource.clip)
yield break;
//在赋值的时候减过fade in time了
int targetTimeSamples = (int)(syncState.StartPlayTime * newSource.clip.frequency);
if (targetTimeSamples >= 0 && targetTimeSamples < newSource.clip.samples)
{
newSource.timeSamples = targetTimeSamples;
}
break;
}
}
onContainerStarted?.Invoke();
if (transition?.FadeInTime > 0f)
{
@@ -112,8 +112,9 @@ namespace OCES.Audio
while (true)
{
bool isLoop = loopsCompleted > 0;
yield return this.m_coroutineHost.StartCoroutine(
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm));
PlayContainerOnce(container, handle.TargetVolume, handle, effectiveBpm, isLoop));
if (handle.Cancelled) yield break;
@@ -139,7 +140,12 @@ namespace OCES.Audio
/// <summary>
/// 播放一个 Container 一轮(不含循环逻辑)
/// </summary>
IEnumerator PlayContainerOnce(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlayContainerOnce(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
if (container.Segments == null || container.Segments.Count == 0)
yield break;
@@ -151,11 +157,11 @@ namespace OCES.Audio
break;
case ContainerType.Sequence:
yield return PlaySequence(container, volumeScale, handle, inheritedBpm);
yield return PlaySequence(container, volumeScale, handle, inheritedBpm, isLoop);
break;
case ContainerType.Random:
yield return PlayRandom(container, volumeScale, handle, inheritedBpm);
yield return PlayRandom(container, volumeScale, handle, inheritedBpm, isLoop);
break;
}
}
@@ -211,7 +217,12 @@ namespace OCES.Audio
// Sequence
// ─────────────────────────────────────────────
IEnumerator PlaySequence(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlaySequence(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
bool isStep = container.ContainerPlayMode;
@@ -219,7 +230,7 @@ namespace OCES.Audio
{
// Step: 每次只播一个,游标全局推进
int index = GetNextSequenceIndex(container);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(container.Segments[index], volumeScale, handle, inheritedBpm, isLoop);
}
else
@@ -233,7 +244,7 @@ namespace OCES.Audio
if (i > 0 && container.StrategyParam > 0)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(container.Segments[i], volumeScale, handle, inheritedBpm, isLoop);
}
}
}
@@ -242,7 +253,12 @@ namespace OCES.Audio
// Random
// ─────────────────────────────────────────────
IEnumerator PlayRandom(MusicContainer container, float volumeScale, ContainerPlayHandle handle, float inheritedBpm)
IEnumerator PlayRandom(
MusicContainer container,
float volumeScale,
ContainerPlayHandle handle,
float inheritedBpm,
bool isLoop = false)
{
bool isStep = container.ContainerPlayMode; // 同上,音乐系统默认 Continuous
@@ -250,7 +266,7 @@ namespace OCES.Audio
{
// Step Random: 随机选一个播放,算一次 loopCount
uint chosen = PickRandomChild(container);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
}
else
{
@@ -268,7 +284,7 @@ namespace OCES.Audio
if (container.StrategyParam > 0 && remaining.Count < container.Segments.Count - 1)
yield return new WaitForSeconds(container.StrategyParam);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm);
yield return PlayChildAndWait(chosen, volumeScale, handle, inheritedBpm, isLoop);
}
}
}
@@ -280,10 +296,14 @@ namespace OCES.Audio
/// <summary>
/// 播放一个子元素(segment 或 container),等待其完成后返回。
/// </summary>
IEnumerator PlayChildAndWait(uint id, float volumeScale, ContainerPlayHandle parentHandle, float inheritedBpm)
IEnumerator PlayChildAndWait(uint id,
float volumeScale,
ContainerPlayHandle parentHandle,
float inheritedBpm,
bool isLoop = false)
{
bool done = false;
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm);
ContainerPlayHandle child = PlayChild(id, volumeScale, () => done = true, inheritedBpm, isLoop);
if (child != null)
parentHandle.ChildHandles.Add(child);
@@ -299,17 +319,17 @@ namespace OCES.Audio
/// <summary>
/// 启动一个子元素的播放,不等待,返回句柄。
/// </summary>
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm)
ContainerPlayHandle PlayChild(uint id, float volumeScale, Action onDone, float inheritedBpm, bool isLoop = false)
{
// ID < 1000000 是 MusicSegment,否则是嵌套 Container
return id < 1000000u ? PlaySegment(id, volumeScale, onDone) : Play(id, onDone, inheritedBpm: inheritedBpm);
return id < 1000000u ? PlaySegment(id, volumeScale, onDone, isLoop) : Play(id, onDone, inheritedBpm);
}
// ─────────────────────────────────────────────
// Segment 播放
// ─────────────────────────────────────────────
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished)
ContainerPlayHandle PlaySegment(uint segmentId, float volumeScale, Action onFinished, bool isLoop = false)
{
MusicSegment segment = this.m_segmentConfig.QueryById(segmentId);
if (segment == null)
@@ -333,7 +353,12 @@ namespace OCES.Audio
source.volume = volumeScale;
source.Play();
var handle = new ContainerPlayHandle();
if (isLoop && segment.StartOffset > 0)
{
source.time = (float)segment.StartOffset;
}
ContainerPlayHandle handle = new();
handle.ActiveSources.Add(source);
handle.Coroutine = this.m_coroutineHost.StartCoroutine(
WaitSegmentFinish(source, handle, onFinished, segment.EndOffset));
@@ -342,7 +367,7 @@ namespace OCES.Audio
IEnumerator WaitSegmentFinish(AudioSource source, ContainerPlayHandle handle, Action onFinished, double endOffset)
{
double effectiveTime = endOffset > 0f ? source.clip.length - endOffset : 0f;
double effectiveTime = endOffset > 0f ? source.clip.length - endOffset : source.clip.length;
// 1. 等待"逻辑结束"EndOffset 或自然结束)
yield return new WaitWhile(() =>
@@ -14,6 +14,7 @@ namespace OCES.Audio
{
readonly MusicContainerConfig m_containerConfig;
readonly MusicTransitionConfig m_transitionConfig;
readonly MusicSegmentConfig m_segmentConfig;
readonly MonoBehaviour m_coroutineHost;
readonly ChannelFader m_fader;
readonly BeatClock m_beatClock;
@@ -30,6 +31,7 @@ namespace OCES.Audio
internal MusicChannelPlayer(
MusicContainerConfig containerConfig,
MusicSegmentConfig segmentConfig,
MusicTransitionConfig transitionConfig,
LongAudioContainerPlayer player,
MonoBehaviour coroutineHost,
@@ -39,6 +41,7 @@ namespace OCES.Audio
{
this.m_containerConfig = containerConfig;
this.m_transitionConfig = transitionConfig;
this.m_segmentConfig = segmentConfig;
this.m_coroutineHost = coroutineHost;
this.m_fader = new ChannelFader(player, coroutineHost);
this.m_beatClock = new BeatClock(coroutineHost, onBeat, onBar, onGrid);
@@ -55,6 +58,7 @@ namespace OCES.Audio
/// </summary>
internal void SwitchTo(uint newContainerId)
{
Debug.Log($"[MusicChannelPlayer] SwitchTo({newContainerId}): CurrentContainerId={this.m_fader.CurrentContainerId}, CurrentHandle={this.m_fader.CurrentHandle}");
if (newContainerId == this.m_fader.CurrentContainerId && this.m_fader.CurrentHandle != null)
return; // 已经在播目标,无需切换
@@ -110,6 +114,7 @@ namespace OCES.Audio
if (syncState.Mode == SyncPoint.SameAsCurrentSegment)
{
Debug.Log($"[MusicChannelPlayer] DoTransition L117: CurrentHandle={this.m_fader.CurrentHandle}, CurrentContainerId={this.m_fader.CurrentContainerId}");
// 旧 Container 不存在(如游戏首次启动),静默降级为 Start
if (this.m_fader.CurrentHandle == null)
{
@@ -129,6 +134,7 @@ namespace OCES.Audio
syncState.BaseTimeSamples = oldSource.timeSamples;
syncState.BaseDspTime = AudioSettings.dspTime;
syncState.SampleRate = oldSource.clip.frequency;
syncState.SourceStartOffset = GetEffectiveStartOffset(this.m_currentContainer);
}
else
{
@@ -162,22 +168,45 @@ namespace OCES.Audio
MusicContainer container = this.m_containerConfig.QueryById(newContainerId);
float bpm = container.Bpm;
// SyncPoint: BeatClock 用调整后的 dspTime,对齐到音频实际播放位置
double newStartOffset = GetEffectiveStartOffset(container);
double dspTime;
if (syncState is { Mode: SyncPoint.SameAsCurrentSegment })
{
double elapsedSeconds = AudioSettings.dspTime - syncState.BaseDspTime;
int elapsedSamples = (int)(elapsedSeconds * syncState.SampleRate);
double audioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate;
dspTime = AudioSettings.dspTime - audioTime;
double currentAudioTime = (double)(syncState.BaseTimeSamples + elapsedSamples) / syncState.SampleRate;
// 当前的source播到了哪里
double currentLogicalTime = Math.Max(0, currentAudioTime - syncState.SourceStartOffset);
// EntryCue之后播了多少秒。要是小于0说明pre-entry还没播完。
double newAudioTime = currentLogicalTime + newStartOffset;
// 新Audio应该从多少秒开始播放
bool isPlayPreEntry = true; //TODO transition 是否播放pre-entry
syncState.TargetAudioTime = isPlayPreEntry ? currentLogicalTime + newStartOffset : currentLogicalTime;
dspTime = AudioSettings.dspTime - newAudioTime;
}
else
{
double startPlayTime;
if (this.m_currentContainer == null)
{
startPlayTime = 0;
}
else
{
double fadeInTime = transition?.FadeInTime ?? 0f;
startPlayTime = newStartOffset - fadeInTime;
startPlayTime = Math.Clamp(startPlayTime, 0, double.PositiveInfinity);
}
syncState.StartPlayTime = startPlayTime;
dspTime = AudioSettings.dspTime;
}
this.m_currentContainer = container;
this.m_beatClock.Restart(container, bpm, dspTime);
this.m_beatClock.Restart(container, bpm, dspTime, newStartOffset);
}));
yield return this.m_currentFadeInCoroutine;
this.m_currentFadeInCoroutine = null;
@@ -227,5 +256,26 @@ namespace OCES.Audio
}
return best;
}
double GetEffectiveStartOffset(MusicContainer container)
{
if (container.ContainerType == ContainerType.Blend)
{
return 0.0;
}
if (container.Segments is not { Count: > 0 })
return 0.0;
uint firstId = container.Segments[0];
if (firstId < 1000000u)
{
MusicSegment segment = this.m_segmentConfig.QueryById(firstId);
return segment?.StartOffset ?? 0.0;
}
MusicContainer child = this.m_containerConfig.QueryById(firstId);
return child != null ? GetEffectiveStartOffset(child) : 0.0;
}
}
}
@@ -0,0 +1,10 @@
namespace OCES.Audio
{
public partial class MusicSegmentConfig
{
// TODO: 运行前边界验证
// - MusicSegment.StartOffset <= AudioClip.length
// - MusicSegment.EndOffset <= AudioClip.length
// - MusicSegment.StartOffset + MusicSegment.EndOffset <= AudioClip.length
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5c388a0ba2794e1ca09ccdde5bd5f99a
timeCreated: 1778049644
@@ -40,7 +40,7 @@ namespace OCES.Audio
this.m_stateRouter = new MusicStateRouter(musicPaths, ambiencePaths);
this.m_musicChannel = new MusicChannelPlayer(
containers, musicTransitions, longAudioContainerPlayer, this,
containers, segments, musicTransitions, longAudioContainerPlayer, this,
id => OnBeat?.Invoke(id),
id => OnBar?.Invoke(id),
id => OnGrid?.Invoke(id));