@tool extends "res://addons/godot_mcp/commands/base_command.gd" func get_commands() -> Dictionary: return { "get_audio_bus_layout": _get_audio_bus_layout, "add_audio_bus": _add_audio_bus, "set_audio_bus": _set_audio_bus, "add_audio_bus_effect": _add_audio_bus_effect, "add_audio_player": _add_audio_player, "get_audio_info": _get_audio_info, } func _get_audio_bus_layout(_params: Dictionary) -> Dictionary: var buses: Array[Dictionary] = [] for i in range(AudioServer.bus_count): var bus_data := { "index": i, "name": AudioServer.get_bus_name(i), "volume_db": AudioServer.get_bus_volume_db(i), "solo": AudioServer.is_bus_solo(i), "mute": AudioServer.is_bus_mute(i), "bypass_effects": AudioServer.is_bus_bypassing_effects(i), "send": AudioServer.get_bus_send(i), "effects": [], } var effects: Array[Dictionary] = [] for j in range(AudioServer.get_bus_effect_count(i)): var effect := AudioServer.get_bus_effect(i, j) var effect_data := { "index": j, "type": effect.get_class(), "enabled": AudioServer.is_bus_effect_enabled(i, j), } # Include effect-specific parameters effect_data["params"] = _get_effect_params(effect) effects.append(effect_data) bus_data["effects"] = effects buses.append(bus_data) return success({"bus_count": AudioServer.bus_count, "buses": buses}) func _get_effect_params(effect: AudioEffect) -> Dictionary: var params := {} if effect is AudioEffectReverb: var rev := effect as AudioEffectReverb params = {"room_size": rev.room_size, "damping": rev.damping, "wet": rev.wet, "dry": rev.dry, "spread": rev.spread} elif effect is AudioEffectDelay: var d := effect as AudioEffectDelay params = {"tap1_active": d.tap1_active, "tap1_delay_ms": d.tap1_delay_ms, "tap1_level_db": d.tap1_level_db, "tap2_active": d.tap2_active, "tap2_delay_ms": d.tap2_delay_ms, "tap2_level_db": d.tap2_level_db} elif effect is AudioEffectCompressor: var c := effect as AudioEffectCompressor params = {"threshold": c.threshold, "ratio": c.ratio, "attack_us": c.attack_us, "release_ms": c.release_ms, "gain": c.gain, "mix": c.mix, "sidechain": c.sidechain} elif effect is AudioEffectLimiter: var l := effect as AudioEffectLimiter params = {"ceiling_db": l.ceiling_db, "threshold_db": l.threshold_db, "soft_clip_db": l.soft_clip_db, "soft_clip_ratio": l.soft_clip_ratio} elif effect is AudioEffectDistortion: var dist := effect as AudioEffectDistortion params = {"mode": dist.mode, "pre_gain": dist.pre_gain, "post_gain": dist.post_gain, "keep_hf_hz": dist.keep_hf_hz, "drive": dist.drive} elif effect is AudioEffectChorus: var ch := effect as AudioEffectChorus params = {"voice_count": ch.voice_count, "dry": ch.dry, "wet": ch.wet} elif effect is AudioEffectPhaser: var ph := effect as AudioEffectPhaser params = {"range_min_hz": ph.range_min_hz, "range_max_hz": ph.range_max_hz, "rate_hz": ph.rate_hz, "feedback": ph.feedback, "depth": ph.depth} elif effect is AudioEffectFilter: # Covers LowPassFilter, HighPassFilter, BandPassFilter, etc. var f := effect as AudioEffectFilter params = {"cutoff_hz": f.cutoff_hz, "resonance": f.resonance, "gain": f.gain, "db": f.db} elif effect is AudioEffectAmplify: var a := effect as AudioEffectAmplify params = {"volume_db": a.volume_db} return params func _add_audio_bus(params: Dictionary) -> Dictionary: var result := require_string(params, "name") if result[1] != null: return result[1] var bus_name: String = result[0] # Check if bus name already exists for i in range(AudioServer.bus_count): if AudioServer.get_bus_name(i) == bus_name: return error_invalid_params("Audio bus '%s' already exists at index %d" % [bus_name, i]) var at_position: int = optional_int(params, "at_position", -1) AudioServer.add_bus(at_position) var idx: int = AudioServer.bus_count - 1 if at_position < 0 else at_position AudioServer.set_bus_name(idx, bus_name) if params.has("volume_db"): AudioServer.set_bus_volume_db(idx, float(params["volume_db"])) var send: String = optional_string(params, "send", "") if not send.is_empty(): AudioServer.set_bus_send(idx, send) if params.has("solo"): AudioServer.set_bus_solo(idx, bool(params["solo"])) if params.has("mute"): AudioServer.set_bus_mute(idx, bool(params["mute"])) return success({"name": bus_name, "index": idx, "bus_count": AudioServer.bus_count}) func _set_audio_bus(params: Dictionary) -> Dictionary: var result := require_string(params, "name") if result[1] != null: return result[1] var bus_name: String = result[0] var idx := AudioServer.get_bus_index(bus_name) if idx < 0: return error_not_found("Audio bus '%s'" % bus_name) var changes := 0 if params.has("volume_db"): AudioServer.set_bus_volume_db(idx, float(params["volume_db"])) changes += 1 if params.has("solo"): AudioServer.set_bus_solo(idx, bool(params["solo"])) changes += 1 if params.has("mute"): AudioServer.set_bus_mute(idx, bool(params["mute"])) changes += 1 if params.has("bypass_effects"): AudioServer.set_bus_bypass_effects(idx, bool(params["bypass_effects"])) changes += 1 var send: String = optional_string(params, "send", "") if not send.is_empty(): AudioServer.set_bus_send(idx, send) changes += 1 if params.has("rename"): var new_name: String = str(params["rename"]) AudioServer.set_bus_name(idx, new_name) bus_name = new_name changes += 1 return success({"name": bus_name, "index": idx, "changes": changes}) func _add_audio_bus_effect(params: Dictionary) -> Dictionary: var result := require_string(params, "bus") if result[1] != null: return result[1] var bus_name: String = result[0] var result2 := require_string(params, "effect_type") if result2[1] != null: return result2[1] var effect_type: String = result2[0] var bus_idx := AudioServer.get_bus_index(bus_name) if bus_idx < 0: return error_not_found("Audio bus '%s'" % bus_name) var effect: AudioEffect = null var effect_params: Dictionary = params.get("params", {}) if params.has("params") else {} match effect_type.to_lower(): "reverb": var e := AudioEffectReverb.new() if effect_params.has("room_size"): e.room_size = float(effect_params["room_size"]) if effect_params.has("damping"): e.damping = float(effect_params["damping"]) if effect_params.has("wet"): e.wet = float(effect_params["wet"]) if effect_params.has("dry"): e.dry = float(effect_params["dry"]) if effect_params.has("spread"): e.spread = float(effect_params["spread"]) effect = e "chorus": var e := AudioEffectChorus.new() if effect_params.has("voice_count"): e.voice_count = int(effect_params["voice_count"]) if effect_params.has("dry"): e.dry = float(effect_params["dry"]) if effect_params.has("wet"): e.wet = float(effect_params["wet"]) effect = e "delay": var e := AudioEffectDelay.new() if effect_params.has("tap1_active"): e.tap1_active = bool(effect_params["tap1_active"]) if effect_params.has("tap1_delay_ms"): e.tap1_delay_ms = float(effect_params["tap1_delay_ms"]) if effect_params.has("tap1_level_db"): e.tap1_level_db = float(effect_params["tap1_level_db"]) if effect_params.has("tap2_active"): e.tap2_active = bool(effect_params["tap2_active"]) if effect_params.has("tap2_delay_ms"): e.tap2_delay_ms = float(effect_params["tap2_delay_ms"]) if effect_params.has("tap2_level_db"): e.tap2_level_db = float(effect_params["tap2_level_db"]) effect = e "compressor": var e := AudioEffectCompressor.new() if effect_params.has("threshold"): e.threshold = float(effect_params["threshold"]) if effect_params.has("ratio"): e.ratio = float(effect_params["ratio"]) if effect_params.has("attack_us"): e.attack_us = float(effect_params["attack_us"]) if effect_params.has("release_ms"): e.release_ms = float(effect_params["release_ms"]) if effect_params.has("gain"): e.gain = float(effect_params["gain"]) if effect_params.has("mix"): e.mix = float(effect_params["mix"]) effect = e "limiter": var e := AudioEffectLimiter.new() if effect_params.has("ceiling_db"): e.ceiling_db = float(effect_params["ceiling_db"]) if effect_params.has("threshold_db"): e.threshold_db = float(effect_params["threshold_db"]) if effect_params.has("soft_clip_db"): e.soft_clip_db = float(effect_params["soft_clip_db"]) if effect_params.has("soft_clip_ratio"): e.soft_clip_ratio = float(effect_params["soft_clip_ratio"]) effect = e "phaser": var e := AudioEffectPhaser.new() if effect_params.has("range_min_hz"): e.range_min_hz = float(effect_params["range_min_hz"]) if effect_params.has("range_max_hz"): e.range_max_hz = float(effect_params["range_max_hz"]) if effect_params.has("rate_hz"): e.rate_hz = float(effect_params["rate_hz"]) if effect_params.has("feedback"): e.feedback = float(effect_params["feedback"]) if effect_params.has("depth"): e.depth = float(effect_params["depth"]) effect = e "distortion": var e := AudioEffectDistortion.new() if effect_params.has("mode"): e.mode = int(effect_params["mode"]) as AudioEffectDistortion.Mode if effect_params.has("pre_gain"): e.pre_gain = float(effect_params["pre_gain"]) if effect_params.has("post_gain"): e.post_gain = float(effect_params["post_gain"]) if effect_params.has("keep_hf_hz"): e.keep_hf_hz = float(effect_params["keep_hf_hz"]) if effect_params.has("drive"): e.drive = float(effect_params["drive"]) effect = e "lowpassfilter", "lowpass": var e := AudioEffectLowPassFilter.new() if effect_params.has("cutoff_hz"): e.cutoff_hz = float(effect_params["cutoff_hz"]) if effect_params.has("resonance"): e.resonance = float(effect_params["resonance"]) effect = e "highpassfilter", "highpass": var e := AudioEffectHighPassFilter.new() if effect_params.has("cutoff_hz"): e.cutoff_hz = float(effect_params["cutoff_hz"]) if effect_params.has("resonance"): e.resonance = float(effect_params["resonance"]) effect = e "bandpassfilter", "bandpass": var e := AudioEffectBandPassFilter.new() if effect_params.has("cutoff_hz"): e.cutoff_hz = float(effect_params["cutoff_hz"]) if effect_params.has("resonance"): e.resonance = float(effect_params["resonance"]) effect = e "amplify": var e := AudioEffectAmplify.new() if effect_params.has("volume_db"): e.volume_db = float(effect_params["volume_db"]) effect = e "eq": var e := AudioEffectEQ.new() effect = e _: return error_invalid_params("Unknown effect type: '%s'. Valid types: reverb, chorus, delay, compressor, limiter, phaser, distortion, lowpassfilter, highpassfilter, bandpassfilter, amplify, eq" % effect_type) var at_position: int = optional_int(params, "at_position", -1) AudioServer.add_bus_effect(bus_idx, effect, at_position) var effect_idx: int = AudioServer.get_bus_effect_count(bus_idx) - 1 if at_position < 0 else at_position return success({"bus": bus_name, "bus_index": bus_idx, "effect_type": effect.get_class(), "effect_index": effect_idx}) func _add_audio_player(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var result2 := require_string(params, "name") if result2[1] != null: return result2[1] var player_name: String = result2[0] var player_type: String = optional_string(params, "type", "AudioStreamPlayer") var valid_types := ["AudioStreamPlayer", "AudioStreamPlayer2D", "AudioStreamPlayer3D"] if player_type not in valid_types: return error_invalid_params("Invalid player type '%s'. Valid: %s" % [player_type, ", ".join(valid_types)]) var parent := find_node_by_path(node_path) if parent == null: return error_not_found("Node at '%s'" % node_path) var root := get_edited_root() if root == null: return error_no_scene() var player: Node = null match player_type: "AudioStreamPlayer": player = AudioStreamPlayer.new() "AudioStreamPlayer2D": player = AudioStreamPlayer2D.new() "AudioStreamPlayer3D": player = AudioStreamPlayer3D.new() player.name = player_name # Set stream if provided var stream_path: String = optional_string(params, "stream", "") if not stream_path.is_empty(): if ResourceLoader.exists(stream_path): var stream = ResourceLoader.load(stream_path) if stream is AudioStream: player.set("stream", stream) else: player.queue_free() return error_invalid_params("Resource at '%s' is not an AudioStream" % stream_path) else: player.queue_free() return error_not_found("Audio stream at '%s'" % stream_path) # Common properties if params.has("volume_db"): player.set("volume_db", float(params["volume_db"])) var bus: String = optional_string(params, "bus", "") if not bus.is_empty(): player.set("bus", bus) if params.has("autoplay"): player.set("autoplay", bool(params["autoplay"])) # 2D-specific properties if player is AudioStreamPlayer2D: if params.has("max_distance"): (player as AudioStreamPlayer2D).max_distance = float(params["max_distance"]) if params.has("attenuation"): (player as AudioStreamPlayer2D).attenuation = float(params["attenuation"]) # 3D-specific properties if player is AudioStreamPlayer3D: if params.has("max_distance"): (player as AudioStreamPlayer3D).max_distance = float(params["max_distance"]) if params.has("attenuation_model"): (player as AudioStreamPlayer3D).attenuation_model = int(params["attenuation_model"]) as AudioStreamPlayer3D.AttenuationModel if params.has("unit_size"): (player as AudioStreamPlayer3D).unit_size = float(params["unit_size"]) add_child_with_undo(parent, player, root, "MCP: Add audio player") return success({ "name": player_name, "type": player_type, "parent": node_path, "stream": stream_path, "bus": player.get("bus"), "volume_db": player.get("volume_db"), "autoplay": player.get("autoplay"), }) func _get_audio_info(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var node := find_node_by_path(node_path) if node == null: return error_not_found("Node at '%s'" % node_path) var players: Array[Dictionary] = [] _collect_audio_players(node, players) return success({"node_path": node_path, "audio_player_count": players.size(), "players": players}) func _collect_audio_players(node: Node, result: Array[Dictionary]) -> void: if node is AudioStreamPlayer or node is AudioStreamPlayer2D or node is AudioStreamPlayer3D: var info := { "name": node.name, "path": str(get_edited_root().get_path_to(node)), "type": node.get_class(), "volume_db": node.get("volume_db"), "bus": node.get("bus"), "autoplay": node.get("autoplay"), "playing": node.get("playing"), "stream": "", } var stream = node.get("stream") if stream != null and stream is AudioStream: info["stream"] = stream.resource_path if node is AudioStreamPlayer2D: info["max_distance"] = (node as AudioStreamPlayer2D).max_distance info["attenuation"] = (node as AudioStreamPlayer2D).attenuation elif node is AudioStreamPlayer3D: info["max_distance"] = (node as AudioStreamPlayer3D).max_distance info["attenuation_model"] = (node as AudioStreamPlayer3D).attenuation_model info["unit_size"] = (node as AudioStreamPlayer3D).unit_size result.append(info) for child in node.get_children(): _collect_audio_players(child, result)