Editor/Tools/WorkflowTools.AudioMixer.cs

Editor MCP tool method for managing audio mixer buses in the active scene. It discovers SoundPointComponent instances, groups them by mixer target, and supports actions: list_buses/status, set_volume, mute, unmute, and duck, returning JSON tool outcomes.

File Access
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		/// <summary>
		/// Manages and queries audio mixer buses (Master, Music, SFX, Voice, UI) across the scene
		/// and audio runtime for sound isolation, volume calibration, and ducking tests.
		/// </summary>
		[McpTool( "pocketknife_audio_mixer_manage" )]
		public static string AudioMixerManage(
			[Description( "Action: 'status', 'list_buses', 'set_volume', 'mute', 'unmute', 'duck'" )] string action = "status",
			[Description( "Target audio bus name (e.g. 'Master', 'Music', 'SFX', 'Voice', 'UI')" )] string bus = "Master",
			[Description( "Target volume from 0.0 (silent) to 1.0 (full)" )] float volume = 1.0f,
			[Description( "Ducking reduction factor from 0.0 to 1.0 (e.g. 0.3 to reduce volume by 70%)" )] float duckFactor = 0.3f )
		{
			try
			{
				var scene = EditorRuntime.ResolveActiveScene();
				var normAction = (action ?? "").ToLowerInvariant().Trim();
				if ( normAction == "status" || string.IsNullOrWhiteSpace( normAction ) )
				{
					normAction = "list_buses";
				}

				// Discover standard and scene-defined audio buses
				var standardBuses = new[] { "Master", "Music", "SFX", "Voice", "UI", "Ambient" };
				var soundPoints = scene?.GetAllComponents<SoundPointComponent>().ToList() ?? new List<SoundPointComponent>();

				var busGroups = new Dictionary<string, List<SoundPointComponent>>( StringComparer.OrdinalIgnoreCase );
				foreach ( var b in standardBuses )
				{
					busGroups[b] = new List<SoundPointComponent>();
				}

				foreach ( var sp in soundPoints )
				{
					var targetStr = sp.TargetMixer.ToString();
					var target = string.IsNullOrWhiteSpace( targetStr ) || targetStr == "unknown" ? "SFX" : targetStr;
					if ( !busGroups.TryGetValue( target, out var list ) )
					{
						list = new List<SoundPointComponent>();
						busGroups[target] = list;
					}
					list.Add( sp );
				}

				if ( normAction == "list_buses" )
				{
					var busSummaries = busGroups.Select( kvp => new
					{
						Bus = kvp.Key,
						ActiveSourceCount = kvp.Value.Count,
						AverageVolume = kvp.Value.Count > 0 ? kvp.Value.Average( s => s.Volume ) : 1.0f,
						ActiveSources = kvp.Value.Select( s => new
						{
							GameObject = s.GameObject?.Name,
							Volume = s.Volume,
							Pitch = s.Pitch,
							Distance = s.Distance
						} ).ToArray()
					} ).ToArray();

					return McpJson.Write( ToolOutcome<object>.Ready( new
					{
						Action = "list_buses",
						TotalBuses = busSummaries.Length,
						TotalSoundPoints = soundPoints.Count,
						Buses = busSummaries,
						Success = true
					} ) );
				}

				var targetBus = bus.Trim();
				var clampedVol = Math.Clamp( volume, 0.0f, 2.0f );

				if ( normAction == "set_volume" )
				{
					int modifiedCount = 0;
					if ( targetBus.Equals( "Master", StringComparison.OrdinalIgnoreCase ) )
					{
						foreach ( var sp in soundPoints )
						{
							sp.Volume = clampedVol;
							modifiedCount++;
						}
					}
					else if ( busGroups.TryGetValue( targetBus, out var targetList ) )
					{
						foreach ( var sp in targetList )
						{
							sp.Volume = clampedVol;
							modifiedCount++;
						}
					}

					return McpJson.Write( ToolOutcome<object>.Ready( new
					{
						Action = "set_volume",
						Bus = targetBus,
						NewVolume = clampedVol,
						SourcesModified = modifiedCount,
						Success = true
					} ) );
				}

				if ( normAction == "mute" || normAction == "unmute" )
				{
					var isMute = normAction == "mute";
					var targetVol = isMute ? 0.0f : 1.0f;
					int modifiedCount = 0;

					if ( targetBus.Equals( "Master", StringComparison.OrdinalIgnoreCase ) )
					{
						foreach ( var sp in soundPoints )
						{
							sp.Volume = targetVol;
							modifiedCount++;
						}
					}
					else if ( busGroups.TryGetValue( targetBus, out var targetList ) )
					{
						foreach ( var sp in targetList )
						{
							sp.Volume = targetVol;
							modifiedCount++;
						}
					}

					return McpJson.Write( ToolOutcome<object>.Ready( new
					{
						Action = normAction,
						Bus = targetBus,
						IsMuted = isMute,
						SourcesModified = modifiedCount,
						Success = true
					} ) );
				}

				if ( normAction == "duck" )
				{
					var factor = Math.Clamp( duckFactor, 0.0f, 1.0f );
					int duckedCount = 0;

					if ( busGroups.TryGetValue( targetBus, out var targetList ) )
					{
						foreach ( var sp in targetList )
						{
							sp.Volume *= factor;
							duckedCount++;
						}
					}

					return McpJson.Write( ToolOutcome<object>.Ready( new
					{
						Action = "duck",
						Bus = targetBus,
						DuckFactor = factor,
						SourcesDucked = duckedCount,
						Success = true
					} ) );
				}

				return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "audio.invalid_action", $"Unsupported action '{action}'. Use 'status', 'list_buses', 'set_volume', 'mute', 'unmute', or 'duck'." ) );
			}
			catch ( Exception ex )
			{
				return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "audio.mixer_failed", "Audio mixer operation failed.", ex ) );
			}
		}
	}
}