Files
bansonic_beta_main/Assets/scripts/Audio/BgmSpectrumVisualizer.cs
T

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
}
}