extends Control # === User-tweakable exports === @export var music_bus := "MusicBus" @export var num_points := 128 # base samples taken from analyzer @export var samples_per_segment := 3 # higher -> smoother interpolation @export var line_width := 3.0 @export var line_color := Color(0.2, 0.8, 1.0) @export var fill_color := Color(0.2, 0.8, 1.0, 0.18) @export var base_amplitude_scale := 0.8 # baseline (fraction of Control height) @export var auto_scale := true # automatically adjust scale to avoid clipping @export var auto_target_fraction := 0.85 # fraction of half-height we want peak to occupy @export var auto_max_multiplier := 3.0 @export var scale_smoothness := 0.08 # smoothing for auto scale changes @export var value_smoothness := 0.18 # smoothing for spectrum magnitudes @export var spectrum_gain := 5.0 # multiplier on analyzer magnitude @export var baseline_smoothness := 0.05 # smoothing for baseline centering # === Internal === @onready var analyzer = AudioServer.get_bus_effect_instance( AudioServer.get_bus_index(music_bus), 0 ) var values: Array = [] var current_amplitude_scale: float = base_amplitude_scale var baseline: float = 0.0 func _ready() -> void: values.resize(num_points) for i in range(num_points): values[i] = 0.0 func _process(delta: float) -> void: if not analyzer: return # --- sample spectrum and smooth values --- for i in range(num_points): var freq_low: float = lerp(20.0, 20000.0, float(i) / num_points) var freq_high: float = lerp(20.0, 20000.0, float(i + 1) / num_points) var mag_v: float = analyzer.get_magnitude_for_frequency_range(freq_low, freq_high).length() * spectrum_gain values[i] = lerp(values[i], mag_v, value_smoothness) # --- update auto scale (smoothed) --- _update_auto_scale() # --- update dynamic baseline for centering --- _update_baseline() queue_redraw() func _update_auto_scale() -> void: if not auto_scale: current_amplitude_scale = lerp(current_amplitude_scale, base_amplitude_scale, scale_smoothness) return var peak: float = 0.0 for v in values: if v > peak: peak = v var eps: float = 1e-6 var half_h: float = size.y * 0.5 var peak_pixels: float = peak * size.y * base_amplitude_scale var target_pixels: float = half_h * auto_target_fraction var required_mul: float = (target_pixels / (peak_pixels + eps)) required_mul = clamp(required_mul, 1.0 / auto_max_multiplier, auto_max_multiplier) var new_scale: float = base_amplitude_scale * required_mul current_amplitude_scale = lerp(current_amplitude_scale, new_scale, scale_smoothness) func _update_baseline() -> void: var avg_mag: float = 0.0 for v in values: avg_mag += v avg_mag /= num_points baseline = lerp(baseline, avg_mag, baseline_smoothness) # --- Catmull-Rom interpolation for smooth curves --- func _catmull_rom_interpolate(raw_points: PackedVector2Array, samples_per_segment: int) -> PackedVector2Array: var n: int = raw_points.size() if n < 2: return raw_points.duplicate() var out := PackedVector2Array() for i in range(n - 1): var p0: Vector2 = raw_points[clamp(i - 1, 0, n - 1)] var p1: Vector2 = raw_points[i] var p2: Vector2 = raw_points[i + 1] var p3: Vector2 = raw_points[clamp(i + 2, 0, n - 1)] for s in range(samples_per_segment): var t: float = float(s) / samples_per_segment var t2: float = t * t var t3: float = t2 * t var x: float = 0.5 * ( (2.0 * p1.x) + (-p0.x + p2.x) * t + (2.0*p0.x - 5.0*p1.x + 4.0*p2.x - p3.x) * t2 + (-p0.x + 3.0*p1.x - 3.0*p2.x + p3.x) * t3 ) var y: float = 0.5 * ( (2.0 * p1.y) + (-p0.y + p2.y) * t + (2.0*p0.y - 5.0*p1.y + 4.0*p2.y - p3.y) * t2 + (-p0.y + 3.0*p1.y - 3.0*p2.y + p3.y) * t3 ) out.append(Vector2(x, y)) out.append(raw_points[n - 1]) return out func _draw() -> void: if values.is_empty(): return var mid_y: float = size.y * 0.5 var step_x: float = size.x / float(num_points - 1) # raw top points (before interpolation) var raw := PackedVector2Array() for i in range(num_points): var x: float = i * step_x var y: float = mid_y - (values[i] - baseline) * size.y * current_amplitude_scale raw.append(Vector2(x, y)) # smooth the curve var top_points: PackedVector2Array = _catmull_rom_interpolate(raw, samples_per_segment) # mirrored bottom points (reverse order) var bottom_points: PackedVector2Array = PackedVector2Array() for i in range(top_points.size() - 1, -1, -1): var p: Vector2 = top_points[i] bottom_points.append(Vector2(p.x, 2.0 * mid_y - p.y)) # build filled polygon var poly: PackedVector2Array = PackedVector2Array() poly.append(Vector2(0, mid_y)) for p in top_points: poly.append(p) poly.append(Vector2(size.x, mid_y)) for p in bottom_points: poly.append(p) # draw filled area draw_polygon(poly, [fill_color]) # draw top and bottom outlines if top_points.size() >= 2: draw_polyline(top_points, line_color, line_width, true) draw_polyline(bottom_points, line_color, line_width, true)