@@ -26247,15 +26247,15 @@ MonoBehaviour:
|
|||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: 81e2cda70e8084a3fb324d644db244fd
|
- guid: 81e2cda70e8084a3fb324d644db244fd
|
||||||
type: 5
|
type: 5
|
||||||
m_fileInfoHash: 418726.asset
|
m_fileInfoHash: 402948.asset
|
||||||
m_assetbundle:
|
m_assetbundle:
|
||||||
m_addressable:
|
m_addressable:
|
||||||
m_atlas:
|
m_atlas:
|
||||||
m_fileSize: 418726
|
m_fileSize: 402948
|
||||||
m_assetChangeTS: 1780591987
|
m_assetChangeTS: 1782667802
|
||||||
m_fileInfoReadTS: 1780592004
|
m_fileInfoReadTS: 1782667803
|
||||||
m_fileWriteTS: 1780591987
|
m_fileWriteTS: 1782667802
|
||||||
m_cachefileWriteTS: 1780591987
|
m_cachefileWriteTS: 1782667802
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList:
|
UseGUIDsList:
|
||||||
- guid: e6b16dc7c203450459bb86e24305f9ca
|
- guid: e6b16dc7c203450459bb86e24305f9ca
|
||||||
@@ -27720,8 +27720,6 @@ MonoBehaviour:
|
|||||||
ids: 1c000000d5000000
|
ids: 1c000000d5000000
|
||||||
- guid: c50c7c9c8871c91409e4470d3c721189
|
- guid: c50c7c9c8871c91409e4470d3c721189
|
||||||
ids: d50000001c000000
|
ids: d50000001c000000
|
||||||
- guid: 5ba4ce13e345fc6488c516cc37c35015
|
|
||||||
ids:
|
|
||||||
- guid: 90539df1cd5704abcb25fec9f3f5f84b
|
- guid: 90539df1cd5704abcb25fec9f3f5f84b
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: af7181f404f1447c0a7a17b3070b952b
|
- guid: af7181f404f1447c0a7a17b3070b952b
|
||||||
@@ -27734,49 +27732,29 @@ MonoBehaviour:
|
|||||||
ids: 1c000000d5000000
|
ids: 1c000000d5000000
|
||||||
- guid: f24cddf7c09a6442abbce710a6fd7397
|
- guid: f24cddf7c09a6442abbce710a6fd7397
|
||||||
ids: 72000000
|
ids: 72000000
|
||||||
- guid: bc99ff1458babbf418cadaa025d9c4fc
|
|
||||||
ids: 73000000
|
|
||||||
- guid: d3b6d8ae350f6e64fa36b94b021a99b1
|
|
||||||
ids:
|
|
||||||
- guid: 87e4ec14fb3f1ed42ad998feb07506f5
|
|
||||||
ids:
|
|
||||||
- guid: 55d0ca39526c5e94ebac2e6da0c8d80c
|
|
||||||
ids:
|
|
||||||
- guid: 6cdb8cc0b3fb21f49b1a761000205509
|
|
||||||
ids: 73000000
|
|
||||||
- guid: 5b33b2e663e78774c9f0c9af55018725
|
- guid: 5b33b2e663e78774c9f0c9af55018725
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: e64d4b6c88d9d47488d476cf81f30f27
|
|
||||||
ids:
|
|
||||||
- guid: ee774914fec90eb469a4484a6d367804
|
|
||||||
ids:
|
|
||||||
- guid: c78150f2237d29247b0f01c770f06979
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: f87b5805002ec9649bcb1b96d8a16ba9
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: 02f78a9ec76d40f49bfac78c64a754d1
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: 3ee40aa79cd242a5b53b0b0ca4f13f0f
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: ee148e281f3c41c5b4ff5f8a5afe5a6c
|
- guid: ee148e281f3c41c5b4ff5f8a5afe5a6c
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 809d29b9ca1b74947aca02225d2ec233
|
- guid: bfbc02946587c834b9028fbc33c1f778
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: b0f85f7da5042c0448a526d5ecacbfa9
|
- guid: 7a5d15f3cf0772b4eaea6396d1733b62
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 0a39a4452fd810640afd1be6e700edee
|
- guid: 8def4223843d3e344a2883973bab7044
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: f66c4aa44c09b6a42a2b5b9f9334d824
|
- guid: d179c8744f837da49ab92aae04d1ae1c
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: e956b4f7a85075c43a444a9f05cc765a
|
- guid: fd0b697a2761664448953c532f9ec2e6
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: a7ec9e7ad8b847b7ae4510af83c5d868
|
- guid: 4ba445ec22724064abf465fd30453dee
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 3028dc075ba8c584d9bc7d1e0255e038
|
- guid: c28442b4549be4344b6e88d9da085145
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 93f09189124b21e479fc891dbc1b93bf
|
- guid: d6fe99a0451d1e2408d25b464b325df6
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: dcc0ed9263c0d524aabeb5f4bd75ecee
|
- guid: 75a3d246cddedc142acd8c58c7fc46c5
|
||||||
|
ids: 1c000000
|
||||||
|
- guid: 9985919b04d78b04ba8451808f2bc34d
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 8113366f9f7cec647878e3af2fb98922
|
- guid: 8113366f9f7cec647878e3af2fb98922
|
||||||
type: 9
|
type: 9
|
||||||
@@ -154522,15 +154500,15 @@ MonoBehaviour:
|
|||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: 99c9720ab356a0642a771bea13969a05
|
- guid: 99c9720ab356a0642a771bea13969a05
|
||||||
type: 3
|
type: 3
|
||||||
m_fileInfoHash: 1266553.unity
|
m_fileInfoHash: 1263123.unity
|
||||||
m_assetbundle:
|
m_assetbundle:
|
||||||
m_addressable:
|
m_addressable:
|
||||||
m_atlas:
|
m_atlas:
|
||||||
m_fileSize: 1266553
|
m_fileSize: 1263123
|
||||||
m_assetChangeTS: 1780591987
|
m_assetChangeTS: 1782667802
|
||||||
m_fileInfoReadTS: 1780592004
|
m_fileInfoReadTS: 1782667803
|
||||||
m_fileWriteTS: 1780591987
|
m_fileWriteTS: 1782667802
|
||||||
m_cachefileWriteTS: 1780591987
|
m_cachefileWriteTS: 1782667802
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList:
|
UseGUIDsList:
|
||||||
- guid: 06a20df987b317044a05ea694c976948
|
- guid: 06a20df987b317044a05ea694c976948
|
||||||
@@ -154559,6 +154537,10 @@ MonoBehaviour:
|
|||||||
ids: 72000000
|
ids: 72000000
|
||||||
- guid: 306cc8c2b49d7114eaa3623786fc2126
|
- guid: 306cc8c2b49d7114eaa3623786fc2126
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
|
- guid: 1344c3c82d62a2a41a3576d8abb8e3ea
|
||||||
|
ids: 73000000
|
||||||
|
- guid: 9985919b04d78b04ba8451808f2bc34d
|
||||||
|
ids: 1c000000
|
||||||
- guid: dc42784cf147c0c48a680349fa168899
|
- guid: dc42784cf147c0c48a680349fa168899
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: 0cd44c1031e13a943bb63640046fad76
|
- guid: 0cd44c1031e13a943bb63640046fad76
|
||||||
@@ -155217,8 +155199,6 @@ MonoBehaviour:
|
|||||||
ids:
|
ids:
|
||||||
- guid: 31321ba15b8f8eb4c954353edc038b1d
|
- guid: 31321ba15b8f8eb4c954353edc038b1d
|
||||||
ids: 15000000
|
ids: 15000000
|
||||||
- guid: 1344c3c82d62a2a41a3576d8abb8e3ea
|
|
||||||
ids: 73000000
|
|
||||||
- guid: e823cd5b5d27c0f4b8256e7c12ee3e6d
|
- guid: e823cd5b5d27c0f4b8256e7c12ee3e6d
|
||||||
ids: 15000000
|
ids: 15000000
|
||||||
- guid: 0406db5a14f94604a8c57ccfbc9f3b46
|
- guid: 0406db5a14f94604a8c57ccfbc9f3b46
|
||||||
@@ -156029,8 +156009,6 @@ MonoBehaviour:
|
|||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: 90539df1cd5704abcb25fec9f3f5f84b
|
- guid: 90539df1cd5704abcb25fec9f3f5f84b
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: 5ba4ce13e345fc6488c516cc37c35015
|
|
||||||
ids:
|
|
||||||
- guid: 91b11ec37fbc84f4fb6ba3b2eb5dc514
|
- guid: 91b11ec37fbc84f4fb6ba3b2eb5dc514
|
||||||
ids: 1c000000d5000000
|
ids: 1c000000d5000000
|
||||||
- guid: 0270f3eb8f22ae74890226a42c0b6da4
|
- guid: 0270f3eb8f22ae74890226a42c0b6da4
|
||||||
@@ -156095,43 +156073,27 @@ MonoBehaviour:
|
|||||||
ids: 1c000000d5000000
|
ids: 1c000000d5000000
|
||||||
- guid: c50c7c9c8871c91409e4470d3c721189
|
- guid: c50c7c9c8871c91409e4470d3c721189
|
||||||
ids: d50000001c000000
|
ids: d50000001c000000
|
||||||
- guid: bc99ff1458babbf418cadaa025d9c4fc
|
|
||||||
ids: 73000000
|
|
||||||
- guid: d3b6d8ae350f6e64fa36b94b021a99b1
|
|
||||||
ids:
|
|
||||||
- guid: 87e4ec14fb3f1ed42ad998feb07506f5
|
|
||||||
ids:
|
|
||||||
- guid: 55d0ca39526c5e94ebac2e6da0c8d80c
|
|
||||||
ids:
|
|
||||||
- guid: 6cdb8cc0b3fb21f49b1a761000205509
|
|
||||||
ids: 73000000
|
|
||||||
- guid: 5b33b2e663e78774c9f0c9af55018725
|
- guid: 5b33b2e663e78774c9f0c9af55018725
|
||||||
ids: 73000000
|
ids: 73000000
|
||||||
- guid: e64d4b6c88d9d47488d476cf81f30f27
|
- guid: 7a5d15f3cf0772b4eaea6396d1733b62
|
||||||
ids:
|
|
||||||
- guid: ee774914fec90eb469a4484a6d367804
|
|
||||||
ids:
|
|
||||||
- guid: c78150f2237d29247b0f01c770f06979
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: f87b5805002ec9649bcb1b96d8a16ba9
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: 02f78a9ec76d40f49bfac78c64a754d1
|
|
||||||
ids: 1c000000
|
|
||||||
- guid: 3ee40aa79cd242a5b53b0b0ca4f13f0f
|
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: ee148e281f3c41c5b4ff5f8a5afe5a6c
|
- guid: ee148e281f3c41c5b4ff5f8a5afe5a6c
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 809d29b9ca1b74947aca02225d2ec233
|
- guid: bfbc02946587c834b9028fbc33c1f778
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: b0f85f7da5042c0448a526d5ecacbfa9
|
- guid: 8def4223843d3e344a2883973bab7044
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 0a39a4452fd810640afd1be6e700edee
|
- guid: d179c8744f837da49ab92aae04d1ae1c
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: f66c4aa44c09b6a42a2b5b9f9334d824
|
- guid: fd0b697a2761664448953c532f9ec2e6
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: a7ec9e7ad8b847b7ae4510af83c5d868
|
- guid: 4ba445ec22724064abf465fd30453dee
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 3028dc075ba8c584d9bc7d1e0255e038
|
- guid: c28442b4549be4344b6e88d9da085145
|
||||||
|
ids: 1c000000
|
||||||
|
- guid: d6fe99a0451d1e2408d25b464b325df6
|
||||||
|
ids: 1c000000
|
||||||
|
- guid: 75a3d246cddedc142acd8c58c7fc46c5
|
||||||
ids: 1c000000
|
ids: 1c000000
|
||||||
- guid: 990a2f951c73f0449a8596028e51c11e
|
- guid: 990a2f951c73f0449a8596028e51c11e
|
||||||
type: 9
|
type: 9
|
||||||
@@ -228637,15 +228599,15 @@ MonoBehaviour:
|
|||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: 2e498d1c8094910479dc3e1b768306a4
|
- guid: 2e498d1c8094910479dc3e1b768306a4
|
||||||
type: 5
|
type: 5
|
||||||
m_fileInfoHash: 58274.asset
|
m_fileInfoHash: 601127.asset
|
||||||
m_assetbundle:
|
m_assetbundle:
|
||||||
m_addressable:
|
m_addressable:
|
||||||
m_atlas:
|
m_atlas:
|
||||||
m_fileSize: 58274
|
m_fileSize: 601127
|
||||||
m_assetChangeTS: 1781933757
|
m_assetChangeTS: 1782668251
|
||||||
m_fileInfoReadTS: 1781933832
|
m_fileInfoReadTS: 1782668252
|
||||||
m_fileWriteTS: 1781933756
|
m_fileWriteTS: 1782668251
|
||||||
m_cachefileWriteTS: 1781933756
|
m_cachefileWriteTS: 1782668251
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList:
|
UseGUIDsList:
|
||||||
- guid: fe393ace9b354375a9cb14cdbbc28be4
|
- guid: fe393ace9b354375a9cb14cdbbc28be4
|
||||||
@@ -235836,15 +235798,15 @@ MonoBehaviour:
|
|||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: aee0e6022210846a7bd6e0e20af73e3f
|
- guid: aee0e6022210846a7bd6e0e20af73e3f
|
||||||
type: 2
|
type: 2
|
||||||
m_fileInfoHash: 13879.cs
|
m_fileInfoHash: 15542.cs
|
||||||
m_assetbundle:
|
m_assetbundle:
|
||||||
m_addressable:
|
m_addressable:
|
||||||
m_atlas:
|
m_atlas:
|
||||||
m_fileSize: 13879
|
m_fileSize: 15542
|
||||||
m_assetChangeTS: 1778983877
|
m_assetChangeTS: 1782667773
|
||||||
m_fileInfoReadTS: 1778983898
|
m_fileInfoReadTS: 1782667781
|
||||||
m_fileWriteTS: 1773552443
|
m_fileWriteTS: 1782667747
|
||||||
m_cachefileWriteTS: 1773552443
|
m_cachefileWriteTS: 1782667747
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: ae11041fa452ac544a8266d1a18181e2
|
- guid: ae11041fa452ac544a8266d1a18181e2
|
||||||
@@ -262402,10 +262364,10 @@ MonoBehaviour:
|
|||||||
m_addressable:
|
m_addressable:
|
||||||
m_atlas:
|
m_atlas:
|
||||||
m_fileSize: 381782
|
m_fileSize: 381782
|
||||||
m_assetChangeTS: 1781942149
|
m_assetChangeTS: 1782668232
|
||||||
m_fileInfoReadTS: 1781942158
|
m_fileInfoReadTS: 1782668240
|
||||||
m_fileWriteTS: 1781941920
|
m_fileWriteTS: 1782668162
|
||||||
m_cachefileWriteTS: 1781941920
|
m_cachefileWriteTS: 1782668162
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
- guid: d0a10b3bf033c7d4189fb87826e48c15
|
- guid: d0a10b3bf033c7d4189fb87826e48c15
|
||||||
@@ -262447,6 +262409,19 @@ MonoBehaviour:
|
|||||||
m_cachefileWriteTS: 1781942146
|
m_cachefileWriteTS: 1781942146
|
||||||
refreshStamp: 2
|
refreshStamp: 2
|
||||||
UseGUIDsList: []
|
UseGUIDsList: []
|
||||||
|
- guid: 890ea908890460d418719dd90440863b
|
||||||
|
type: 2
|
||||||
|
m_fileInfoHash: 35060.cs
|
||||||
|
m_assetbundle:
|
||||||
|
m_addressable:
|
||||||
|
m_atlas:
|
||||||
|
m_fileSize: 35060
|
||||||
|
m_assetChangeTS: 1782668232
|
||||||
|
m_fileInfoReadTS: 1782668240
|
||||||
|
m_fileWriteTS: 1782668119
|
||||||
|
m_cachefileWriteTS: 1782668119
|
||||||
|
refreshStamp: 2
|
||||||
|
UseGUIDsList: []
|
||||||
setting:
|
setting:
|
||||||
alternateColor: 1
|
alternateColor: 1
|
||||||
excludeTypes: 0
|
excludeTypes: 0
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
615
Assets/Scripts/DynamicUI/Timeline/AudioMelSpectrogram.cs
Normal file
615
Assets/Scripts/DynamicUI/Timeline/AudioMelSpectrogram.cs
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
namespace Ichni.Editor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 非 MonoBehaviour 的 Mel 频谱图渲染器。
|
||||||
|
///
|
||||||
|
/// === 架构 ===
|
||||||
|
/// PathNodeCurveDrawer.Update()
|
||||||
|
/// └─ AudioMelSpectrogram.UpdateView(curT, maxT)
|
||||||
|
/// ├─ 首次检测 AudioClip → 后台线程预计算全曲 FFT
|
||||||
|
/// ├─ 每帧从缓存取出当前可见时间段 → 颜色 LUT → Color32[] → SetPixelData → Apply
|
||||||
|
/// └─ 视角未变时整帧跳过
|
||||||
|
///
|
||||||
|
/// 后台线程:PCM 读取(主线程) → FFT + Mel 滤波 + Log(子线程) → 缓存 Log 能量
|
||||||
|
/// 主线程: 按 visibleStart/End 映射到帧范围 → 逐列查 LUT → 写像素 → GPU upload
|
||||||
|
/// </summary>
|
||||||
|
public class AudioMelSpectrogram
|
||||||
|
{
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [公共参数] 由 PathNodeCurveDrawer 每帧通过字段赋值同步
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>渲染目标的 RawImage。尺寸从此读取 × resolutionScale 决定纹理大小。</summary>
|
||||||
|
public RawImage targetImage;
|
||||||
|
|
||||||
|
/// <summary>纹理分辨率缩放。0.5 = 半分辨率(宽/高各砍一半,像素数 1/4)。</summary>
|
||||||
|
[Range(0.1f, 1f)] public float resolutionScale = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>颜色渐变:归一化能量 [0,1] → Color。Inspector 修改实时生效。</summary>
|
||||||
|
public Gradient colorGradient;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [FFT / Mel 常量] 改动需要重新预计算才会生效
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>FFT 窗口大小(采样点数)。越大频率分辨率越高、时间分辨率越低。
|
||||||
|
/// 4096 @ 44100Hz ≈ 93ms 窗口,频率分辨率 ≈ 10.8Hz。
|
||||||
|
/// 配合 512 帧移(87.5% overlap)维持时间分辨率 ~11.6ms。</summary>
|
||||||
|
private const int FFT_SIZE = 4096;
|
||||||
|
|
||||||
|
/// <summary>帧移(相邻 FFT 窗口的起始采样偏移量)。
|
||||||
|
/// 512 = 50% overlap(HOP_SIZE = FFT_SIZE / 2)。
|
||||||
|
/// 每帧对应时间 = HOP_SIZE / sampleRate ≈ 11.6ms。</summary>
|
||||||
|
private const int HOP_SIZE = 512;
|
||||||
|
|
||||||
|
/// <summary>Mel 滤波器组数量。也是缓存和纹理的高度方向分辨率。
|
||||||
|
/// 128 个三角滤波器在 Mel 尺度上等距分布,覆盖 0 ~ sampleRate/2 Hz。
|
||||||
|
/// 更高 → 频率细节更多,但低频段相邻 bin 间距更窄(~40Hz),
|
||||||
|
/// 对钢琴相邻半音(~2Hz @ 440Hz)仍偏粗,但已是实用上限。</summary>
|
||||||
|
private const int MEL_BINS = 128;
|
||||||
|
|
||||||
|
/// <summary>Mel 尺度常数:m = 2595 × log10(1 + f/700)。</summary>
|
||||||
|
private const float MEL_A = 2595f;
|
||||||
|
|
||||||
|
/// <summary>Mel 尺度常数:f = 700 × (10^(m/2595) - 1)。</summary>
|
||||||
|
private const float MEL_B = 700f;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [预计算缓存] 后台线程写入 → 主线程只读
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>当前已缓存的 AudioClip 引用。检测 clip 变化时触发重新预计算。</summary>
|
||||||
|
private AudioClip _cachedClip;
|
||||||
|
|
||||||
|
/// <summary>预计算完成后的 Log-Mel 能量缓存。
|
||||||
|
/// 布局:flat[ frameIndex × MEL_BINS + melBin ],每个元素 = Log(能量)。
|
||||||
|
/// 存 Log 值避免每帧重复 Mathf.Log。volatile 保证主线程读到最新值。</summary>
|
||||||
|
private volatile float[] _logMelData;
|
||||||
|
|
||||||
|
/// <summary>后台线程正在写入的临时缓存。完成后通过 _cacheReady 交给主线程。</summary>
|
||||||
|
private float[] _pendingLogData;
|
||||||
|
|
||||||
|
/// <summary>后台线程完成标记。主线程检测到此标志后切 _pendingLogData → data。</summary>
|
||||||
|
private volatile bool _cacheReady;
|
||||||
|
|
||||||
|
/// <summary>总帧数。由 (totalSamples - FFT_SIZE) / HOP_SIZE + 1 计算。
|
||||||
|
/// 决定了 data 的第一维长度。</summary>
|
||||||
|
private int _totalFrames;
|
||||||
|
|
||||||
|
/// <summary>每帧对应的时间(秒)= HOP_SIZE / sampleRate。
|
||||||
|
/// frame f 的中心时间 = (f + 1) × _timePerFrame。</summary>
|
||||||
|
private float _timePerFrame;
|
||||||
|
|
||||||
|
/// <summary>Mel 三角滤波器组。_melFilters[m][i] = 第 m 个 mel bin 在 FFT bin i 上的权重。
|
||||||
|
/// 在预计算开始时构建,之后只读。后台线程安全(写入完成后才启动线程)。</summary>
|
||||||
|
private float[][] _melFilters;
|
||||||
|
|
||||||
|
/// <summary>AudioClip 的采样率(Hz)。用于 _timePerFrame 和 Mel 滤波器组的频率轴计算。</summary>
|
||||||
|
private int _sampleRate;
|
||||||
|
|
||||||
|
/// <summary>后台线程正在运行的标记。防止重复启动线程。</summary>
|
||||||
|
private volatile bool _computing;
|
||||||
|
|
||||||
|
/// <summary>后台线程已计算完成的帧数。主线程用此判断可以读 _pendingLogData 的前 N 帧。
|
||||||
|
/// 后台线程每算完一批(每 16 帧)通过 Volatile.Write 更新,
|
||||||
|
/// 主线程通过 Volatile.Read 读取,保证写 release/读 acquire 语义。</summary>
|
||||||
|
private volatile int _framesComputed;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [运行时纹理]
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>实际写入的 Texture2D。尺寸 = RawImage 像素尺寸 × resolutionScale。
|
||||||
|
/// 纹理格式 RGBA32,FilterMode 可切换为 Point 避免插值模糊。</summary>
|
||||||
|
private Texture2D _texture;
|
||||||
|
|
||||||
|
/// <summary>像素缓冲区(CPU 侧)。每帧通过 unsafe 指针直接写入,
|
||||||
|
/// 然后 SetPixelData 整块提交到 GPU。SetPixelData 比 SetPixels 快因为走原始字节拷贝。</summary>
|
||||||
|
private Color32[] _pixels32;
|
||||||
|
|
||||||
|
/// <summary>当前纹理尺寸。用于检测 RawImage 尺寸变化并重建纹理。</summary>
|
||||||
|
private int _texW, _texH;
|
||||||
|
|
||||||
|
/// <summary>上一帧渲染的 FFT 帧范围。用于帧级抖动过滤:
|
||||||
|
/// 当 songTime 在帧边界附近振荡时,startF/endF 不变就不重绘,
|
||||||
|
/// 避免子帧级变化导致整张图反复左右偏移。</summary>
|
||||||
|
private int _prevStartF = -1, _prevEndF = -1;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [颜色 LUT] 256 级预计算查找表
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>256 级颜色查找表。norm[0,1] → LUT[(int)(norm × 255)]。
|
||||||
|
/// 避免每像素调用 Gradient.Evaluate(函数调用 + 键插值开销)。</summary>
|
||||||
|
private Color32[] _colorLut;
|
||||||
|
|
||||||
|
/// <summary>上次构建 LUT 时使用的 Gradient 引用。
|
||||||
|
/// 为 null 或与当前 colorGradient 不同时重建。
|
||||||
|
/// 注意:Inspector 修改 Gradient 时不改变引用,所以用了直接引用 + 用户 Gradient 每次重建的策略。</summary>
|
||||||
|
private Gradient _appliedGradient;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// [Bin 查找表] y → melBin 的预计算映射
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>纹理行 y → Mel 滤波器索引。_binForY[y] = round(y / h × MEL_BINS)。
|
||||||
|
/// 避免每像素重复计算浮点除法和 round。</summary>
|
||||||
|
private int[] _binForY;
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// 每帧入口
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>每帧由 PathNodeCurveDrawer.Update() 调用。</summary>
|
||||||
|
/// <param name="audioSource">从 ModuleTime 获取的 AudioSource,用于检测 AudioClip。</param>
|
||||||
|
/// <param name="visibleStart">可见范围起点(秒),通常 = ModuleTime.SmoothTime(与 CurrentBeatF 同源)。</param>
|
||||||
|
/// <param name="visibleEnd">可见范围终点(秒),由 PathNodeCurveDrawer 按 beatRange × 60/BPM 换算。</param>
|
||||||
|
public void UpdateView(AudioSource audioSource, float visibleStart, float visibleEnd, bool forceUpdate = false)
|
||||||
|
{
|
||||||
|
if (targetImage == null) return;
|
||||||
|
|
||||||
|
// ── 检测新 AudioClip → 启动后台预计算 ──────────────────────────
|
||||||
|
// _cachedClip 记录上次预计算的 clip。只要 clip 引用变了就触发重新计算。
|
||||||
|
// _computing 防重入,后台线程运行期间不重复启动。
|
||||||
|
if (audioSource != null && audioSource.clip != null && audioSource.clip != _cachedClip)
|
||||||
|
{
|
||||||
|
if (!_computing)
|
||||||
|
StartPreCompute(audioSource.clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 后台线程完成 → 切缓存 ──────────────────────────────────────
|
||||||
|
// 必须放在 _computing 检查之前:先完成缓存切换,再让 _computing check 通过。
|
||||||
|
if (_cacheReady)
|
||||||
|
{
|
||||||
|
_logMelData = _pendingLogData;
|
||||||
|
_pendingLogData = null;
|
||||||
|
_totalFrames = _logMelData.Length / MEL_BINS;
|
||||||
|
_cachedClip = audioSource.clip;
|
||||||
|
_cacheReady = false;
|
||||||
|
_computing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 完全态:全缓存就绪,正常渲染 ────────────────────────────
|
||||||
|
if (!_computing && _logMelData != null && _totalFrames > 0)
|
||||||
|
{
|
||||||
|
RenderCurrentView(visibleStart, visibleEnd, forceUpdate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 部分态:后台计算中,但已有部分帧可供显示 ────────────────────
|
||||||
|
if (_computing && _pendingLogData != null && _framesComputed > 0)
|
||||||
|
{
|
||||||
|
// 用 _pendingLogData 渲染已完成部分
|
||||||
|
RenderCurrentView(visibleStart, visibleEnd, true,
|
||||||
|
_pendingLogData, _framesComputed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// 渲染当前视图(被完全态和部分态共用)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>根据 visibleStart/End 计算帧范围,渲染到纹理并提交 GPU。</summary>
|
||||||
|
/// <param name="data">数据源:完全态用 data,部分态用 _pendingLogData。</param>
|
||||||
|
/// <param name="framesAvail">可用帧数:完全态 = _totalFrames,部分态 = _framesComputed。</param>
|
||||||
|
private void RenderCurrentView(float visibleStart, float visibleEnd, bool forceUpdate,
|
||||||
|
float[] data = null, int framesAvail = 0)
|
||||||
|
{
|
||||||
|
// 默认走完全态 _logMelData / _totalFrames
|
||||||
|
if (data == null) { data = _logMelData; framesAvail = _totalFrames; }
|
||||||
|
if (data == null || framesAvail == 0) return;
|
||||||
|
|
||||||
|
// ── 可见时间范围 → FFT 帧范围 ─────────────────────────────────
|
||||||
|
// FFT 帧 f 的时间中心 = (f × HOP_SIZE + FFT_SIZE/2) / sampleRate
|
||||||
|
// 当 FFT_SIZE=4096, HOP_SIZE=512: 帧 f 中心在 (f + 4) × _timePerFrame
|
||||||
|
float frameOffset = FFT_SIZE / (2f * HOP_SIZE);
|
||||||
|
// 先算帧号,后判断跳过。帧级检测能过滤 songTime 在帧边界附近的振荡,
|
||||||
|
// 避免 startF/endF 没变时整张图反复左右偏移。
|
||||||
|
int startF = Mathf.Max(0, Mathf.RoundToInt(visibleStart / _timePerFrame - frameOffset));
|
||||||
|
int endF = Mathf.Max(1, Mathf.RoundToInt(visibleEnd / _timePerFrame - frameOffset));
|
||||||
|
|
||||||
|
// ── 帧号未变 → 跳过重绘(部分态 forceUpdate=true 不会跳过)──
|
||||||
|
if (!forceUpdate && startF == _prevStartF && endF == _prevEndF && _pixels32 != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int visFrames = endF - startF;
|
||||||
|
float dur = visibleEnd - visibleStart;
|
||||||
|
if (dur <= 0f) return;
|
||||||
|
|
||||||
|
// ── 纹理尺寸 ──
|
||||||
|
var rect = targetImage.rectTransform.rect;
|
||||||
|
int w = Mathf.Max(1, Mathf.RoundToInt(rect.width * resolutionScale));
|
||||||
|
int h = Mathf.Max(1, Mathf.RoundToInt(rect.height * resolutionScale));
|
||||||
|
bool sizeChanged = _texture == null || _texture.width != w || _texture.height != h;
|
||||||
|
|
||||||
|
if (sizeChanged)
|
||||||
|
InitTexture(w, h);
|
||||||
|
|
||||||
|
// ── 全量重绘 ──
|
||||||
|
BuildColorLut();
|
||||||
|
RenderColumns(startF, visFrames, w, h, 0, w - 1, data, framesAvail);
|
||||||
|
_texture.SetPixelData(_pixels32, 0);
|
||||||
|
_texture.Apply(false);
|
||||||
|
|
||||||
|
_prevStartF = startF;
|
||||||
|
_prevEndF = endF;
|
||||||
|
_texW = w;
|
||||||
|
_texH = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// 列渲染(热路径)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>渲染 colFrom~colTo 列的像素。被 UpdateView 每帧调用(全量重绘)。</summary>
|
||||||
|
/// <param name="startF">可见范围起点帧号</param>
|
||||||
|
/// <param name="visFrames">可见范围总帧数</param>
|
||||||
|
/// <param name="w">纹理像素宽度</param>
|
||||||
|
/// <param name="h">纹理像素高度</param>
|
||||||
|
/// <param name="colFrom">起始列(含)</param>
|
||||||
|
/// <param name="colTo">结束列(含)</param>
|
||||||
|
private void RenderColumns(int startF, int visFrames, int w, int h,
|
||||||
|
int colFrom, int colTo,
|
||||||
|
float[] data, int totalFrames)
|
||||||
|
{
|
||||||
|
// 在堆上分配每列的临时缓冲区。
|
||||||
|
// 相比 stackalloc 有少量 GC 压力,但在 h ≤ 128、每帧最多调用一次的情况下可以忽略。
|
||||||
|
// 用途:一趟读 data 同时写入 colVals + 扫描 min/max,
|
||||||
|
// 第二趟从 colVals 读(避免第二趟再访存托管数组)。
|
||||||
|
float[] colVals = new float[h];
|
||||||
|
|
||||||
|
for (int x = colFrom; x <= colTo; x++)
|
||||||
|
{
|
||||||
|
// 计算当前列对应的 FFT 帧号
|
||||||
|
float t = (float)x / w; // 归一化列位置 [0,1]
|
||||||
|
int frameIdx = startF + (int)math.round(t * visFrames);
|
||||||
|
// 超出已计算帧 → 跳过该列,维持透明背景
|
||||||
|
if (frameIdx >= totalFrames) continue;
|
||||||
|
if (frameIdx < 0) continue;
|
||||||
|
int rowBase = frameIdx * MEL_BINS; // 该帧在 data 中的起始偏移
|
||||||
|
|
||||||
|
// ── 一趟读入 stack + 扫描 min/max ──
|
||||||
|
// 遍历 h 行,每行用 _binForY 映射到 Mel bin,
|
||||||
|
// 从 data 取值 → 存入 colVals[y] → 同时更新 mn/mx。
|
||||||
|
float mn = float.MaxValue, mx = float.MinValue;
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
float v = data[rowBase + _binForY[y]];
|
||||||
|
colVals[y] = v;
|
||||||
|
if (v < mn) mn = v;
|
||||||
|
if (v > mx) mx = v;
|
||||||
|
}
|
||||||
|
// 防止全静音帧导致除零。invRange 在后续乘 norm,不额外乘除。
|
||||||
|
float invRange = 1f / math.max(mx - mn, 0.001f);
|
||||||
|
|
||||||
|
// ── 从 stack 渲染 ──
|
||||||
|
// norm[0,1] → idx[0,255] → LUT[idx] → 写入像素。
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
float norm = (colVals[y] - mn) * invRange;
|
||||||
|
int idx = (int)(norm * 255);
|
||||||
|
if (idx > 255) idx = 255;
|
||||||
|
_pixels32[y * w + x] = _colorLut[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// 后台预计算
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>启动后台线程预计算。
|
||||||
|
/// 主线程:读取 PCM + 混单声道 + 构建 Mel 滤波器组。
|
||||||
|
/// 子线程:Hann 加窗 → FFT → 功率谱 → Mel 三角滤波 → Log → 写入 _pendingLogData。</summary>
|
||||||
|
private void StartPreCompute(AudioClip clip)
|
||||||
|
{
|
||||||
|
_computing = true;
|
||||||
|
_cacheReady = false;
|
||||||
|
_framesComputed = 0; // 重置进度计数器
|
||||||
|
_cachedClip = clip;
|
||||||
|
_sampleRate = clip.frequency;
|
||||||
|
_timePerFrame = (float)HOP_SIZE / _sampleRate; // ≈ 11.6ms @44100Hz
|
||||||
|
|
||||||
|
// 构建 Mel 滤波器组(主线程,因为 _melFilters 不是 volatile)
|
||||||
|
BuildMelFilterBank();
|
||||||
|
|
||||||
|
// ── 主线程:读取 PCM 并混为单声道 ──
|
||||||
|
// AudioClip.GetData() 必须在主线程调用(Unity API 限制)。
|
||||||
|
// clip.samples = 每声道的采样数,totalSamples × channels = 总 float 数。
|
||||||
|
int totalSamples = clip.samples;
|
||||||
|
int channels = clip.channels;
|
||||||
|
float[] samples = new float[totalSamples * channels];
|
||||||
|
clip.GetData(samples, 0);
|
||||||
|
|
||||||
|
// 多声道 → 单声道:取各声道平均值。
|
||||||
|
float[] mono = new float[totalSamples];
|
||||||
|
if (channels == 1)
|
||||||
|
System.Array.Copy(samples, mono, totalSamples);
|
||||||
|
else
|
||||||
|
for (int i = 0; i < totalSamples; i++)
|
||||||
|
{
|
||||||
|
float sum = 0f;
|
||||||
|
for (int c = 0; c < channels; c++)
|
||||||
|
sum += samples[i * channels + c];
|
||||||
|
mono[i] = sum / channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总帧数 = 能容纳的 HOP_SIZE 步进数
|
||||||
|
// 最后一帧的起始偏移 = totalSamples - FFT_SIZE,不足一帧时返回至少 1 帧。
|
||||||
|
int totalFrames = Mathf.Max(1, (totalSamples - FFT_SIZE) / HOP_SIZE + 1);
|
||||||
|
float[] pending = new float[totalFrames * MEL_BINS];
|
||||||
|
_pendingLogData = pending; // 立即暴露给主线程读取,线程写内容 + 更新 _framesComputed
|
||||||
|
|
||||||
|
// ── 后台线程:FFT + Mel 滤波 + Log ──
|
||||||
|
// 所有数据(mono, window, _melFilters)在线程启动前已就绪,不需要同步。
|
||||||
|
// 只写 pending[],不碰任何 Unity API。
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
// Blackman-Harris(4 项最小旁瓣窗):旁瓣 ≤ -92dB,远优于 Hann 的 -32dB。
|
||||||
|
// 代价:主瓣宽度比 Hann 宽约 60%,但 FFT_SIZE=4096 下仍只有 ~22Hz,
|
||||||
|
// 远窄于 Mel 滤波器间距,不影响频率分辨率。
|
||||||
|
float[] window = new float[FFT_SIZE];
|
||||||
|
float bhA0 = 0.35875f, bhA1 = 0.48829f, bhA2 = 0.14128f, bhA3 = 0.01168f;
|
||||||
|
float invN1 = 1f / (FFT_SIZE - 1);
|
||||||
|
for (int i = 0; i < FFT_SIZE; i++)
|
||||||
|
{
|
||||||
|
float p = Mathf.PI * 2f * i * invN1;
|
||||||
|
window[i] = bhA0 - bhA1 * Mathf.Cos(p) + bhA2 * Mathf.Cos(2f * p) - bhA3 * Mathf.Cos(3f * p);
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] real = new float[FFT_SIZE];
|
||||||
|
float[] imag = new float[FFT_SIZE];
|
||||||
|
float[] power = new float[FFT_SIZE / 2];
|
||||||
|
|
||||||
|
for (int f = 0; f < totalFrames; f++)
|
||||||
|
{
|
||||||
|
int offset = f * HOP_SIZE;
|
||||||
|
|
||||||
|
// 加窗:mono[offset + i] × window[i] → real[i]
|
||||||
|
for (int i = 0; i < FFT_SIZE; i++)
|
||||||
|
{
|
||||||
|
int idx = offset + i;
|
||||||
|
real[i] = idx < totalSamples ? mono[idx] * window[i] : 0f;
|
||||||
|
imag[i] = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原位 Cooley-Tukey radix-2 FFT → real/imag 为复频谱
|
||||||
|
FFT(real, imag);
|
||||||
|
|
||||||
|
// 功率谱 |X|² = real² + imag²(只取前 N/2 个正频率 bin)
|
||||||
|
for (int i = 0; i < FFT_SIZE / 2; i++)
|
||||||
|
power[i] = real[i] * real[i] + imag[i] * imag[i];
|
||||||
|
|
||||||
|
// Mel 三角滤波:每个 Mel bin 是若干 FFT bin 的加权和
|
||||||
|
int row = f * MEL_BINS;
|
||||||
|
for (int m = 0; m < MEL_BINS; m++)
|
||||||
|
{
|
||||||
|
float e = 0f;
|
||||||
|
var filter = _melFilters[m];
|
||||||
|
for (int i = 0; i < FFT_SIZE / 2; i++)
|
||||||
|
e += power[i] * filter[i];
|
||||||
|
// 存 Log(energy) 而不是 raw energy!
|
||||||
|
// 人耳对声压的感知是对数的,且 Log 压缩后帧循环不再需要 Mathf.Log。
|
||||||
|
pending[row + m] = Mathf.Log(Mathf.Max(e, 1e-10f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每算完 16 帧(≈ 186ms @11.6ms/帧),更新进度计数器。
|
||||||
|
// Volatile.Write 保证对主线程可见,同时隐含 release 语义,
|
||||||
|
// 确保写 pending[] 的 stores 在写 _framesComputed 之前完成。
|
||||||
|
if ((f & 15) == 0 || f == totalFrames - 1)
|
||||||
|
_framesComputed = f + 1; // volatile 写入,主线程立即可见
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheReady = true; // 通知主线程
|
||||||
|
});
|
||||||
|
thread.IsBackground = true; // 应用程序退出时自动终止
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// Mel 三角滤波器组
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>构建 Mel 三角滤波器组。
|
||||||
|
/// 在 Mel 尺度上均匀放置 MEL_BINS 个三角滤波器,每个覆盖 3 个 Mel 点(左/中心/右)。
|
||||||
|
/// 频率范围:0 ~ sampleRate/2 Hz。
|
||||||
|
/// 三角峰值为 1(归一化),相邻滤波器间交叠 ≈ 50%。</summary>
|
||||||
|
private void BuildMelFilterBank()
|
||||||
|
{
|
||||||
|
int half = FFT_SIZE / 2;
|
||||||
|
float freqMax = _sampleRate / 2f; // Nyquist 频率
|
||||||
|
float melMin = FreqToMel(0f);
|
||||||
|
float melMax = FreqToMel(freqMax);
|
||||||
|
|
||||||
|
// 每个 FFT bin 的频率(Hz)
|
||||||
|
float[] binFreq = new float[half];
|
||||||
|
for (int i = 0; i < half; i++)
|
||||||
|
binFreq[i] = (float)i / FFT_SIZE * _sampleRate;
|
||||||
|
|
||||||
|
_melFilters = new float[MEL_BINS][];
|
||||||
|
for (int m = 0; m < MEL_BINS; m++)
|
||||||
|
{
|
||||||
|
// 在 Mel 尺度上等距取三个点:左 / 中心 / 右
|
||||||
|
float mL = melMin + (melMax - melMin) * (m) / (MEL_BINS + 1);
|
||||||
|
float mC = melMin + (melMax - melMin) * (m + 1) / (MEL_BINS + 1);
|
||||||
|
float mR = melMin + (melMax - melMin) * (m + 2) / (MEL_BINS + 1);
|
||||||
|
float fL = MelToFreq(mL), fC = MelToFreq(mC), fR = MelToFreq(mR);
|
||||||
|
|
||||||
|
// 三角滤波器:左→中心上升段,中心→右下降段,其余为 0
|
||||||
|
var filter = new float[half];
|
||||||
|
for (int i = 0; i < half; i++)
|
||||||
|
{
|
||||||
|
float f = binFreq[i];
|
||||||
|
if (f >= fL && f < fC)
|
||||||
|
filter[i] = (f - fL) / (fC - fL); // 上升
|
||||||
|
else if (f >= fC && f < fR)
|
||||||
|
filter[i] = (fR - f) / (fR - fC); // 下降
|
||||||
|
// else: 0
|
||||||
|
}
|
||||||
|
_melFilters[m] = filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// FFT — 实序列原位 Cooley-Tukey radix-2 DIT(时域抽取)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>一维实序列 FFT(正变换)。
|
||||||
|
/// 输入:real[i] = 实部(时域信号),imag[i] = 0。
|
||||||
|
/// 输出:real[i] + j×imag[i] = 复频谱(0 ~ sampleRate,对称)。
|
||||||
|
/// 只需要前 N/2 个 bin(0 ~ Nyquist)做功率谱计算。
|
||||||
|
///
|
||||||
|
/// radix-2 = 输入长度必须为 2 的幂(FFT_SIZE = 1024 ✅)。
|
||||||
|
/// DIT = 先 bit-reversal 重排,再逐级蝶形运算。</summary>
|
||||||
|
private static void FFT(float[] real, float[] imag)
|
||||||
|
{
|
||||||
|
int n = real.Length;
|
||||||
|
|
||||||
|
// 1. Bit-reversal 重排:使蝶形运算的输入自然有序
|
||||||
|
for (int i = 1, j = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
int bit = n >> 1;
|
||||||
|
for (; j >= bit; bit >>= 1) j -= bit;
|
||||||
|
j += bit;
|
||||||
|
if (i < j)
|
||||||
|
{
|
||||||
|
(real[i], real[j]) = (real[j], real[i]);
|
||||||
|
(imag[i], imag[j]) = (imag[j], imag[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 蝶形运算:逐级合并,len = 2 → 4 → 8 → ... → n
|
||||||
|
for (int len = 2; len <= n; len <<= 1)
|
||||||
|
{
|
||||||
|
float ang = 2f * Mathf.PI / len; // 旋转因子角度
|
||||||
|
float wlenR = Mathf.Cos(ang);
|
||||||
|
float wlenI = -Mathf.Sin(ang); // 负号 = 正变换
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i += len)
|
||||||
|
{
|
||||||
|
float wr = 1f, wi = 0f; // 初始旋转因子 = 1
|
||||||
|
int half = len >> 1;
|
||||||
|
for (int j = 0; j < half; j++)
|
||||||
|
{
|
||||||
|
int i1 = i + j; // 偶序号
|
||||||
|
int i2 = i + j + half; // 奇序号
|
||||||
|
// t = W × X[i2]
|
||||||
|
float tr = wr * real[i2] - wi * imag[i2];
|
||||||
|
float ti = wr * imag[i2] + wi * real[i2];
|
||||||
|
// 蝶形:X[i1] = X[i1] + t, X[i2] = X[i1] - t
|
||||||
|
real[i2] = real[i1] - tr;
|
||||||
|
imag[i2] = imag[i1] - ti;
|
||||||
|
real[i1] += tr;
|
||||||
|
imag[i1] += ti;
|
||||||
|
// 更新旋转因子
|
||||||
|
(wr, wi) = (wr * wlenR - wi * wlenI, wr * wlenI + wi * wlenR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// Mel 尺度转换
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>频率(Hz)→ Mel 值。m = 2595 × log10(1 + f/700)。</summary>
|
||||||
|
private static float FreqToMel(float f) => MEL_A * Mathf.Log10(1f + f / MEL_B);
|
||||||
|
|
||||||
|
/// <summary>Mel 值 → 频率(Hz)。f = 700 × (10^(m/2595) - 1)。</summary>
|
||||||
|
private static float MelToFreq(float m) => MEL_B * (Mathf.Pow(10f, m / MEL_A) - 1f);
|
||||||
|
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// 纹理 / LUT / Bin 表
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>创建或重建纹理与辅助表。</summary>
|
||||||
|
private void InitTexture(int w, int h)
|
||||||
|
{
|
||||||
|
// 纹理格式 RGBA32 = 每像素 4 字节(R/G/B/A × byte),与 Color32 结构一致。
|
||||||
|
// mipChain: false = 不生成 mipmap,节省 GPU 内存和 upload 带宽。
|
||||||
|
_texture = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||||||
|
_texture.filterMode = FilterMode.Point; // 无插值,每格锐利显示频率带
|
||||||
|
_texture.wrapMode = TextureWrapMode.Clamp; // 边缘不循环
|
||||||
|
|
||||||
|
_pixels32 = new Color32[w * h];
|
||||||
|
var bg = (Color32)Color.clear; // 全透明背景
|
||||||
|
for (int i = 0; i < _pixels32.Length; i++)
|
||||||
|
_pixels32[i] = bg;
|
||||||
|
_texture.SetPixelData(_pixels32, 0);
|
||||||
|
_texture.Apply(false);
|
||||||
|
|
||||||
|
if (targetImage != null)
|
||||||
|
targetImage.texture = _texture;
|
||||||
|
|
||||||
|
// 预计算 y → bin 索引:每行 y 映射到最近的 Mel 滤波器
|
||||||
|
_binForY = new int[h];
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
_binForY[y] = Mathf.Clamp(
|
||||||
|
Mathf.RoundToInt((float)y / h * MEL_BINS),
|
||||||
|
0, MEL_BINS - 1);
|
||||||
|
|
||||||
|
_appliedGradient = null; // 强制下次重建 LUT
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>构建 256 级颜色 LUT。
|
||||||
|
/// 用户设置了 Gradient → 每次重建(Inspector 修改不改变引用)。
|
||||||
|
/// 使用默认 Gradient → 缓存到静态引用。</summary>
|
||||||
|
private void BuildColorLut()
|
||||||
|
{
|
||||||
|
var grad = colorGradient != null ? colorGradient : GetDefaultGradient();
|
||||||
|
|
||||||
|
// 只在「使用默认 Gradient 且已缓存」时跳过重建
|
||||||
|
if (colorGradient == null && _appliedGradient == grad && _colorLut != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_colorLut = new Color32[256];
|
||||||
|
for (int i = 0; i < 256; i++)
|
||||||
|
_colorLut[i] = (Color32)grad.Evaluate(i / 255f);
|
||||||
|
_appliedGradient = grad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>默认半透渐变:低能透明 → 深紫 → 青蓝 → 青绿 → 金黄 → 白。
|
||||||
|
/// 透明度逐渐升高,叠在曲线下方也不会遮挡。</summary>
|
||||||
|
private static Gradient _defaultGradient;
|
||||||
|
|
||||||
|
private static Gradient GetDefaultGradient()
|
||||||
|
{
|
||||||
|
if (_defaultGradient != null) return _defaultGradient;
|
||||||
|
|
||||||
|
_defaultGradient = new Gradient();
|
||||||
|
_defaultGradient.SetKeys(
|
||||||
|
new GradientColorKey[]
|
||||||
|
{
|
||||||
|
new GradientColorKey(new Color(0.4f, 0.1f, 0.6f), 0f),
|
||||||
|
new GradientColorKey(new Color(0f, 0.6f, 0.8f), 0.35f),
|
||||||
|
new GradientColorKey(new Color(0.2f, 0.9f, 0.5f), 0.6f),
|
||||||
|
new GradientColorKey(new Color(1f, 0.85f,0.3f), 0.8f),
|
||||||
|
new GradientColorKey(Color.white, 1f),
|
||||||
|
},
|
||||||
|
new GradientAlphaKey[]
|
||||||
|
{
|
||||||
|
new GradientAlphaKey(0f, 0f),
|
||||||
|
new GradientAlphaKey(0.5f, 0.15f),
|
||||||
|
new GradientAlphaKey(0.75f,0.4f),
|
||||||
|
new GradientAlphaKey(0.9f, 0.7f),
|
||||||
|
new GradientAlphaKey(1f, 1f),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return _defaultGradient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 890ea908890460d418719dd90440863b
|
||||||
@@ -36,7 +36,16 @@ namespace Ichni.Editor
|
|||||||
|
|
||||||
private string BeatText;
|
private string BeatText;
|
||||||
private string TimeText;
|
private string TimeText;
|
||||||
|
private AudioMelSpectrogram _audioMelSpectrogram = new();
|
||||||
|
|
||||||
|
/// <summary>Mel 频谱图渲染目标 RawImage。请在 Inspector 中指定 Timeline 下的 RawImage 子对象。</summary>
|
||||||
|
public RawImage spectrogramTarget;
|
||||||
|
|
||||||
|
/// <summary>可选的频谱图颜色渐变。为 null 时使用 AudioMelSpectrogram 内置默认渐变。</summary>
|
||||||
|
public Gradient spectrogramGradient;
|
||||||
|
|
||||||
|
/// <summary>频谱图纹理分辨率缩放。0.5 = 半分辨率。</summary>
|
||||||
|
[Range(0.1f, 1f)] public float spectrogramResolutionScale = 0.5f;
|
||||||
|
|
||||||
protected override void Start()
|
protected override void Start()
|
||||||
{
|
{
|
||||||
@@ -50,6 +59,14 @@ namespace Ichni.Editor
|
|||||||
UpdateTime();
|
UpdateTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 初始化 Mel 频谱图 ──
|
||||||
|
if (_audioMelSpectrogram != null)
|
||||||
|
{
|
||||||
|
_audioMelSpectrogram.targetImage = spectrogramTarget;
|
||||||
|
_audioMelSpectrogram.resolutionScale = spectrogramResolutionScale;
|
||||||
|
if (spectrogramGradient != null)
|
||||||
|
_audioMelSpectrogram.colorGradient = spectrogramGradient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public void Update()
|
public void Update()
|
||||||
{
|
{
|
||||||
@@ -65,7 +82,16 @@ namespace Ichni.Editor
|
|||||||
DetectPointer();
|
DetectPointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Mel 频谱图每帧刷新 ──
|
||||||
|
if (_audioMelSpectrogram != null && musicPlayer != null && musicPlayer.audioSource != null && timePointerModule != null)
|
||||||
|
{
|
||||||
|
float visibleWidth = spectrogramTarget != null
|
||||||
|
? spectrogramTarget.rectTransform.rect.width
|
||||||
|
: GetComponent<RectTransform>().rect.width;
|
||||||
|
float visTimeRange = Mathf.Max(1f, visibleWidth / timePointerModule.timePointerInterval * timePerBeat);
|
||||||
|
_audioMelSpectrogram.UpdateView(musicPlayer.audioSource,
|
||||||
|
songTime, songTime + visTimeRange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private void DetectSetRange()
|
private void DetectSetRange()
|
||||||
{
|
{
|
||||||
|
|||||||
163
Assets/StreamingAssets/NotePrefabs/Flick_Prefab.json
Normal file
163
Assets/StreamingAssets/NotePrefabs/Flick_Prefab.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"Note" : {
|
||||||
|
"__type" : "System.Collections.Generic.List`1[[Ichni.RhythmGame.Beatmap.BaseElement_BM, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]],mscorlib",
|
||||||
|
"value" : [
|
||||||
|
{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.Flick_BM,Assembly-CSharp",
|
||||||
|
"availableFlickDirections" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"exactJudgeTime" : 4.5,
|
||||||
|
"elementName" : "New Flick",
|
||||||
|
"tags" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"elementGuid" : {
|
||||||
|
"value" : "f394b21d-b7c4-44c6-96ea-5fc5c10d9e25"
|
||||||
|
},
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.TimeDurationSubmodule_BM,Assembly-CSharp",
|
||||||
|
"isOverridingDuration" : false,
|
||||||
|
"startTime" : -32767,
|
||||||
|
"endTime" : 32767,
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "f394b21d-b7c4-44c6-96ea-5fc5c10d9e25"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.NoteJudgeSubmodule_BM,Assembly-CSharp",
|
||||||
|
"judgeUnitList" : [
|
||||||
|
{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.TouchAreaJudgeUnit_BM,Assembly-CSharp",
|
||||||
|
"areaRadius" : 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "f394b21d-b7c4-44c6-96ea-5fc5c10d9e25"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.NoteAudioSubmodule_BM,Assembly-CSharp",
|
||||||
|
"generalJudgeAudioList" : [
|
||||||
|
"DefaultFlick"
|
||||||
|
],
|
||||||
|
"perfectAudioList" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"goodAudioList" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"badAudioList" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"missAudioList" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"holdStartAudioList" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "f394b21d-b7c4-44c6-96ea-5fc5c10d9e25"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse.Beatmap.DTMNoteVisual_BM,Assembly-CSharp",
|
||||||
|
"isHighlighted" : false,
|
||||||
|
"themeBundleName" : "departure_to_multiverse",
|
||||||
|
"objectName" : "DTM_NoteVisualFlick",
|
||||||
|
"elementName" : "New Note Visual",
|
||||||
|
"tags" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"elementGuid" : {
|
||||||
|
"value" : "424dfc35-dee9-4d76-9f3d-854956cf3627"
|
||||||
|
},
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "f394b21d-b7c4-44c6-96ea-5fc5c10d9e25"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.TransformSubmodule_BM,Assembly-CSharp",
|
||||||
|
"originalPosition" : {
|
||||||
|
"x" : 0,
|
||||||
|
"y" : 0,
|
||||||
|
"z" : 0
|
||||||
|
},
|
||||||
|
"originalEulerAngles" : {
|
||||||
|
"x" : 0,
|
||||||
|
"y" : 0,
|
||||||
|
"z" : 0
|
||||||
|
},
|
||||||
|
"originalScale" : {
|
||||||
|
"x" : 1,
|
||||||
|
"y" : 1,
|
||||||
|
"z" : 1
|
||||||
|
},
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "424dfc35-dee9-4d76-9f3d-854956cf3627"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.TimeDurationSubmodule_BM,Assembly-CSharp",
|
||||||
|
"isOverridingDuration" : false,
|
||||||
|
"startTime" : -32767,
|
||||||
|
"endTime" : 32767,
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "424dfc35-dee9-4d76-9f3d-854956cf3627"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.ColorSubmodule_BM,Assembly-CSharp",
|
||||||
|
"originalBaseColor" : {
|
||||||
|
"r" : 1,
|
||||||
|
"g" : 1,
|
||||||
|
"b" : 1,
|
||||||
|
"a" : 1
|
||||||
|
},
|
||||||
|
"emissionEnabled" : false,
|
||||||
|
"originalEmissionColor" : {
|
||||||
|
"r" : 0,
|
||||||
|
"g" : 0,
|
||||||
|
"b" : 0,
|
||||||
|
"a" : 1
|
||||||
|
},
|
||||||
|
"originalEmissionIntensity" : 0,
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "424dfc35-dee9-4d76-9f3d-854956cf3627"
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
"__type" : "Ichni.RhythmGame.Beatmap.EffectSubmodule_BM,Assembly-CSharp",
|
||||||
|
"effectCollection" : {"Generate":[
|
||||||
|
{
|
||||||
|
"__type" : "Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse.Beatmap.DTMNoteGenerateExpand_BM,Assembly-CSharp",
|
||||||
|
"generateTime" : 0.5,
|
||||||
|
"effectTime" : 0.2
|
||||||
|
}
|
||||||
|
],"GeneralJudge":[
|
||||||
|
|
||||||
|
],"StartHold":[
|
||||||
|
|
||||||
|
],"Holding":[
|
||||||
|
|
||||||
|
],"Perfect":[
|
||||||
|
{
|
||||||
|
"__type" : "Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse.Beatmap.DTMNotePerfectBurst_BM,Assembly-CSharp",
|
||||||
|
"effectTime" : 0
|
||||||
|
}
|
||||||
|
],"Good":[
|
||||||
|
|
||||||
|
],"Bad":[
|
||||||
|
|
||||||
|
],"Miss":[
|
||||||
|
{
|
||||||
|
"__type" : "Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse.Beatmap.DTMNoteMissTransparent_BM,Assembly-CSharp",
|
||||||
|
"effectTime" : 0.2
|
||||||
|
}
|
||||||
|
],"AfterJudge":[
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"attachedElementGuid" : {
|
||||||
|
"value" : "424dfc35-dee9-4d76-9f3d-854956cf3627"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d0a10b3bf033c7d4189fb87826e48c15
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user