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); for (int i = 0; i < bars; i++) { float t = (float)i / (bars - 1); int idx = Mathf.Clamp(Mathf.RoundToInt(Mathf.Lerp(1, spectrumSize - 1, t * t)), 1, spectrumSize - 1); 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(); img.color = Color.white; var outline = go.GetComponent(); outline.effectColor = new Color(0f, 0f, 0f, 1f); outline.effectDistance = new Vector2(1f, 1f); outline.useGraphicAlpha = true; barPrefab = go.GetComponent(); 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); } spectrum = new float[Mathf.Max(64, spectrumSize)]; barRects = new RectTransform[bars]; barHeights = new float[bars]; barBasePos = new Vector2[bars]; for (int i = 0; i < bars; i++) { var bar = Instantiate(barPrefab, transform); bar.gameObject.SetActive(true); barRects[i] = bar; var outline = bar.GetComponent(); if (outline == null) { outline = bar.gameObject.AddComponent(); 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 } }