251 lines
7.5 KiB
C#
251 lines
7.5 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
[ExecuteAlways]
|
|
public class BgmSpectrumVisualizer : MonoBehaviour
|
|
{
|
|
[Header("Audio")]
|
|
public AudioSource source;
|
|
public FFTWindow fftWindow = FFTWindow.BlackmanHarris;
|
|
|
|
[Header("Bars")]
|
|
public RectTransform barPrefab;
|
|
public int bars = 64;
|
|
public float barWidth = 4.8f;
|
|
public float barSpacing = 2f;
|
|
public float maxHeight = 100f;
|
|
|
|
[Header("Spectrum")]
|
|
public int spectrumSize = 512;
|
|
public float amplitude = 60f;
|
|
public float smooth = 12f;
|
|
public float noiseFloor = 0.002f;
|
|
|
|
[Header("Wave")]
|
|
public float waveAmplitude = 6f;
|
|
public float waveSpeed = 1.6f;
|
|
public float waveFrequency = 0.45f;
|
|
[Tooltip("If true, bars expand upward from baseline only.")]
|
|
public bool growUpOnly = true;
|
|
|
|
[Header("Edit Preview")]
|
|
public bool previewInEditMode = true;
|
|
public float previewAmplitude = 0.6f;
|
|
public float previewNoiseSpeed = 0.25f;
|
|
|
|
private float[] spectrum;
|
|
private RectTransform[] barRects;
|
|
private float[] barHeights;
|
|
private Vector2[] barBasePos;
|
|
private bool rebuildQueued;
|
|
|
|
private void Awake()
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
Init();
|
|
}
|
|
else
|
|
{
|
|
ScheduleRebuild();
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (barRects == null || barRects.Length == 0)
|
|
{
|
|
if (Application.isPlaying) Init();
|
|
else ScheduleRebuild();
|
|
}
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
if (bars < 1) bars = 1;
|
|
if (barWidth < 1f) barWidth = 1f;
|
|
if (barSpacing < 0f) barSpacing = 0f;
|
|
if (maxHeight < 1f) maxHeight = 1f;
|
|
|
|
ScheduleRebuild();
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
RebuildBars();
|
|
}
|
|
|
|
private void ScheduleRebuild()
|
|
{
|
|
if (rebuildQueued) return;
|
|
rebuildQueued = true;
|
|
#if UNITY_EDITOR
|
|
EditorApplication.delayCall -= RebuildIfQueued;
|
|
EditorApplication.delayCall += RebuildIfQueued;
|
|
#endif
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void RebuildIfQueued()
|
|
{
|
|
if (!rebuildQueued) return;
|
|
rebuildQueued = false;
|
|
if (this == null) return;
|
|
|
|
RebuildBars();
|
|
if (!Application.isPlaying && previewInEditMode)
|
|
UpdateBarsPreview();
|
|
}
|
|
#endif
|
|
|
|
private void Update()
|
|
{
|
|
if (!Application.isPlaying)
|
|
{
|
|
if (previewInEditMode)
|
|
UpdateBarsPreview();
|
|
return;
|
|
}
|
|
|
|
if (source == null)
|
|
{
|
|
var mgr = BgmPlaybackManager.Instance;
|
|
if (mgr != null) source = mgr.audioSource;
|
|
}
|
|
|
|
if (source == null || source.clip == null)
|
|
return;
|
|
|
|
if (spectrum == null || spectrum.Length != spectrumSize)
|
|
spectrum = new float[spectrumSize];
|
|
|
|
source.GetSpectrumData(spectrum, 0, fftWindow);
|
|
|
|
int barCount = bars;
|
|
int barDenominator = Mathf.Max(1, barCount - 1);
|
|
int spectrumDenominator = Mathf.Max(1, spectrumSize - 1);
|
|
|
|
for (int i = 0; i < barCount; i++)
|
|
{
|
|
float t = (float)i / barDenominator;
|
|
int idx = Mathf.Clamp(Mathf.RoundToInt(Mathf.Lerp(1, spectrumDenominator, t * t)), 1, spectrumDenominator);
|
|
|
|
float v = Mathf.Max(0f, spectrum[idx] - noiseFloor) * amplitude;
|
|
float baseHeight = Mathf.Clamp01(v) * maxHeight;
|
|
|
|
float wave = Mathf.Sin(Time.time * waveSpeed + i * waveFrequency) * waveAmplitude;
|
|
float target = Mathf.Clamp(baseHeight + wave, 0f, maxHeight);
|
|
|
|
barHeights[i] = Mathf.Lerp(barHeights[i], target, Time.deltaTime * smooth);
|
|
|
|
var rt = barRects[i];
|
|
float h = Mathf.Max(1f, barHeights[i]);
|
|
var sd = rt.sizeDelta;
|
|
if (sd.x != barWidth || sd.y != h)
|
|
rt.sizeDelta = new Vector2(barWidth, h);
|
|
if (growUpOnly && barBasePos != null && i < barBasePos.Length)
|
|
{
|
|
var ap = rt.anchoredPosition;
|
|
float nx = barBasePos[i].x;
|
|
float ny = barBasePos[i].y + h * 0.5f;
|
|
if (ap.x != nx || ap.y != ny)
|
|
rt.anchoredPosition = new Vector2(nx, ny);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RebuildBars()
|
|
{
|
|
if (barPrefab == null)
|
|
{
|
|
var go = new GameObject("Bar", typeof(RectTransform), typeof(Image), typeof(Outline));
|
|
var img = go.GetComponent<Image>();
|
|
img.color = Color.white;
|
|
var outline = go.GetComponent<Outline>();
|
|
outline.effectColor = new Color(0f, 0f, 0f, 1f);
|
|
outline.effectDistance = new Vector2(1f, 1f);
|
|
outline.useGraphicAlpha = true;
|
|
barPrefab = go.GetComponent<RectTransform>();
|
|
barPrefab.sizeDelta = new Vector2(barWidth, 10f);
|
|
go.SetActive(false);
|
|
}
|
|
|
|
// destroy existing bars
|
|
for (int i = transform.childCount - 1; i >= 0; i--)
|
|
{
|
|
var child = transform.GetChild(i);
|
|
if (Application.isPlaying)
|
|
Destroy(child.gameObject);
|
|
else
|
|
DestroyImmediate(child.gameObject);
|
|
}
|
|
|
|
int barCount = bars;
|
|
spectrum = new float[Mathf.Max(64, spectrumSize)];
|
|
barRects = new RectTransform[barCount];
|
|
barHeights = new float[barCount];
|
|
barBasePos = new Vector2[barCount];
|
|
|
|
for (int i = 0; i < barCount; i++)
|
|
{
|
|
var bar = Instantiate(barPrefab, transform);
|
|
bar.gameObject.SetActive(true);
|
|
barRects[i] = bar;
|
|
var outline = bar.GetComponent<Outline>();
|
|
if (outline == null)
|
|
{
|
|
outline = bar.gameObject.AddComponent<Outline>();
|
|
outline.effectColor = new Color(0f, 0f, 0f, 1f);
|
|
outline.effectDistance = new Vector2(1f, 1f);
|
|
outline.useGraphicAlpha = true;
|
|
}
|
|
barRects[i].sizeDelta = new Vector2(barWidth, 1f);
|
|
var basePos = new Vector2(i * (barWidth + barSpacing), 0f);
|
|
barRects[i].anchoredPosition = basePos;
|
|
barBasePos[i] = basePos;
|
|
}
|
|
}
|
|
|
|
private void UpdateBarsPreview()
|
|
{
|
|
if (barRects == null || barRects.Length == 0)
|
|
return;
|
|
|
|
float time = GetEditorTime();
|
|
for (int i = 0; i < bars; i++)
|
|
{
|
|
float t = (float)i / (bars - 1);
|
|
float noise = Mathf.PerlinNoise(t * 2.5f, time * previewNoiseSpeed);
|
|
float baseHeight = Mathf.Lerp(0.2f, 1f, noise) * maxHeight * previewAmplitude;
|
|
float wave = Mathf.Sin(time * waveSpeed + i * waveFrequency) * waveAmplitude;
|
|
float target = Mathf.Clamp(baseHeight + wave, 0f, maxHeight);
|
|
barHeights[i] = Mathf.Lerp(barHeights[i], target, 0.2f);
|
|
var rt = barRects[i];
|
|
float h = Mathf.Max(1f, barHeights[i]);
|
|
var sd = rt.sizeDelta;
|
|
if (sd.x != barWidth || sd.y != h)
|
|
rt.sizeDelta = new Vector2(barWidth, h);
|
|
if (growUpOnly && barBasePos != null && i < barBasePos.Length)
|
|
{
|
|
var ap = rt.anchoredPosition;
|
|
float nx = barBasePos[i].x;
|
|
float ny = barBasePos[i].y + h * 0.5f;
|
|
if (ap.x != nx || ap.y != ny)
|
|
rt.anchoredPosition = new Vector2(nx, ny);
|
|
}
|
|
}
|
|
}
|
|
|
|
private float GetEditorTime()
|
|
{
|
|
#if UNITY_EDITOR
|
|
return (float)EditorApplication.timeSinceStartup;
|
|
#else
|
|
return Time.realtimeSinceStartup;
|
|
#endif
|
|
}
|
|
}
|