@tool extends VBoxContainer @export_range(1, 128) var bars_count := 32 var transform: Transform3D: get: return %AudioVisualizer.global_transform @export_exp_easing("inout") var motion_smoothing := 0.025 @export_range(0, 0.5) var bar_thickness := 0.065 @export_range(0, 10) var bars_separation := 0.325 @export_exp_easing("inout") var color_offset_speed := 0.4 @export var colors: Gradient = null var MusicAnalyzerBus := &"MusicAnalyzer" var MasterBus := &"Master" var MAX_HZ := 16000.0 var MIN_HZ := 20.0 var MIN_DB := 60.0 var spectrum: AudioEffectSpectrumAnalyzerInstance = null var smoothed_energy: Array[float] = [] var color_offset := 0.0 var _on_data_loaded_callback = null func _ready(): var bus = AudioServer.get_bus_index(MusicAnalyzerBus) if bus == -1: print("'MusicVisualizer' audio bus not found.\nSet 'VisualizerAudioBus.tres' as the default bus to use the audio visualizer.") spectrum = AudioServer.get_bus_effect_instance(bus, 0) %MuteMaster.button_pressed = AudioServer.is_bus_mute(AudioServer.get_bus_index(MasterBus)) %VolumeSlider.value = db_to_linear(AudioServer.get_bus_volume_db(AudioServer.get_bus_index(MasterBus))) if OS.has_feature('web'): motion_smoothing = motion_smoothing * 1.5 _on_data_loaded_callback = JavaScriptBridge.create_callback(_on_data_loaded) # Retrieve the 'gd_callbacks' object var gdcallbacks: JavaScriptObject = JavaScriptBridge.get_interface("gd_callbacks") # Assign the callbacks gdcallbacks.dataLoaded = _on_data_loaded_callback func _process(_delta): if %MusicPlayer.playing: draw_spectrum() func _pressed(): var open_file = func(filepath: String): print("Opening '%s'" % filepath) var file = FileAccess.open(filepath, FileAccess.READ) var data = file.get_buffer(file.get_length()) open_stream(filepath.get_extension(), data) if DisplayServer.has_feature(DisplayServer.FEATURE_NATIVE_DIALOG): DisplayServer.file_dialog_show("Select audio file", "", "", true, DisplayServer.FILE_DIALOG_MODE_OPEN_FILE, ["*.mp3"], func (status: bool, selected: PackedStringArray, _fileter: int): if status and selected.size(): open_file.call(selected[0]) ) elif OS.has_feature('web'): JavaScriptBridge.eval("loadData()") else: var fd := FileDialog.new() add_child(fd) fd.title = "Select audio file" fd.access = FileDialog.ACCESS_FILESYSTEM fd.file_mode = FileDialog.FILE_MODE_OPEN_FILE fd.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) fd.add_filter("*.mp3") fd.popup_centered_ratio(0.8) fd.file_selected.connect(func(path: String): open_file.call(path) ) fd.visibility_changed.connect(func(): if not fd.visible: fd.queue_free() ) func _on_data_loaded(data: Array) -> void: # Make sure there is something if (data.size() == 0): return var file_name: String = data[0] print("Opening '%s'" % file_name) var arr: PackedByteArray = JavaScriptBridge.eval("gd_callbacks.dataLoadedResult;") open_stream(file_name.get_extension(), arr) func open_stream(file_ext: String, data: PackedByteArray): var stream: AudioStream = null if file_ext == "mp3": stream = AudioStreamMP3.new() stream.data = data if not stream.data: print("Failed to load MP3!") return if not stream: print("Failed to load music!") return %MusicPlayer.stream = stream %MusicPlayer.bus = MusicAnalyzerBus %MusicPlayer.play() # Debugging frequencies for ih in range(1, bars_count + 1): var _hz: float = log_freq(ih / float(bars_count), MIN_HZ, MAX_HZ) #print("%.0f hz %.2f" % [_hz, ih / float(bars_count)]) func draw_spectrum(): var _s1 = DebugDraw3D.scoped_config().set_thickness(bar_thickness).set_center_brightness(0.9) var prev_hz = MIN_HZ smoothed_energy.resize(bars_count) var xf := transform var y := xf.basis.y var h := y.length() var x := xf.basis.x var z := xf.basis.z var origin := xf.origin - (x * bars_count + (x * bars_separation) * (bars_count - 1)) * 0.5 var sum := 0.0 for ih in range(1, bars_count + 1): var i := ih - 1 var hz: float = log_freq(ih / float(bars_count), MIN_HZ, MAX_HZ) var magnitude: float = spectrum.get_magnitude_for_frequency_range(prev_hz, hz, AudioEffectSpectrumAnalyzerInstance.MAGNITUDE_AVERAGE).length() var energy: float = clampf((MIN_DB + linear_to_db(magnitude)) / MIN_DB, 0, 1) var e: float = lerp(smoothed_energy[i], energy, clampf(get_process_delta_time() / motion_smoothing if motion_smoothing else 1.0, 0, 1)) smoothed_energy[i] = e var height: float = e * h sum += e var s := x * bars_separation var a := origin + x * i + s * i + (z * 0.5) var b := origin + x * (i + 1) + s * i + (z * -0.5) + xf.basis.y.normalized() * clampf(height, 0.001, h) var c := Color.HOT_PINK if colors: c = colors.sample(wrapf(float(ih) / bars_count + color_offset, 0, 1)) c.s = clamp(c.s - smoothed_energy[i] * 0.3, 0, 1.0) DebugDraw3D.draw_box_ab(a, b, y, c) prev_hz = hz color_offset = wrapf(color_offset + sum / smoothed_energy.size() * clampf(get_process_delta_time() / color_offset_speed if color_offset_speed else 1.0, 0, 1), 0, 1) func log10(val: float) -> float: return log(val) / 2.302585 func log_freq(pos: float, min_hz: float, max_hz: float) -> float: return pow(10, log10(min_hz) + (log10(max_hz) - log10(min_hz)) * pos) func _on_volume_slider_value_changed(value): AudioServer.set_bus_volume_db(AudioServer.get_bus_index(MasterBus), linear_to_db(value)) func _on_mute_master_toggled(toggled_on): AudioServer.set_bus_mute(AudioServer.get_bus_index(MasterBus), toggled_on)