Editor tool helpers for audio and chat testing. Provides methods to inject a voice audio stream (synthesize tone or read WAV), probe spatial audio between scene objects, and simulate chat actions including sending platform chat and simple filter testing.
#nullable enable
using System;
using System.IO;
using System.Linq;
using Sandbox;
namespace Editor.Mcp
{
internal static class AudioAndChatDriver
{
private static Scene? ResolveScene()
{
if ( Game.ActiveScene != null ) return Game.ActiveScene;
var session = EditorRuntime.GetStaticProperty( "Editor.SceneEditorSession", "Active" );
if ( session != null )
{
return EditorRuntime.GetMember( session, "Scene" ) as Scene;
}
return null;
}
public static ToolOutcome<object> InjectVoiceStream(
string mode = "tone",
float durationSeconds = 1.0f,
int sampleRate = 44100,
float frequencyHz = 440f,
string? filePath = null,
float volume = 1.0f,
float pitch = 1.0f,
string channel = "all",
bool mute = false )
{
try
{
var clampedDuration = Math.Clamp( durationSeconds, 0.1f, 10.0f );
var clampedRate = Math.Clamp( sampleRate, 8000, 48000 );
var clampedVol = Math.Clamp( volume, 0.0f, 2.0f );
var normMode = mode.ToLowerInvariant().Trim();
short[] pcmSamples;
if ( mute || normMode == "silence" )
{
pcmSamples = new short[(int)(clampedRate * clampedDuration)];
}
else if ( normMode == "wav_file" && !string.IsNullOrWhiteSpace( filePath ) && File.Exists( filePath ) )
{
var fileBytes = File.ReadAllBytes( filePath );
// Check WAV header (RIFF)
if ( fileBytes.Length > 44 && fileBytes[0] == 'R' && fileBytes[1] == 'I' && fileBytes[2] == 'F' && fileBytes[3] == 'F' )
{
// Skip standard 44-byte WAV header
var sampleCount = (fileBytes.Length - 44) / 2;
pcmSamples = new short[sampleCount];
Buffer.BlockCopy( fileBytes, 44, pcmSamples, 0, pcmSamples.Length * 2 );
}
else
{
var sampleCount = fileBytes.Length / 2;
pcmSamples = new short[sampleCount];
Buffer.BlockCopy( fileBytes, 0, pcmSamples, 0, pcmSamples.Length * 2 );
}
}
else // "tone" (synthesize sine wave burst)
{
var totalSamples = (int)(clampedRate * clampedDuration);
pcmSamples = new short[totalSamples];
var angularFreq = 2.0 * Math.PI * Math.Clamp( frequencyHz, 20f, 10000f ) / clampedRate;
for ( int i = 0; i < totalSamples; i++ )
{
// Soft envelope fade in/out to avoid popping
var envelope = 1.0;
if ( i < 200 ) envelope = (double)i / 200;
else if ( i > totalSamples - 200 ) envelope = (double)(totalSamples - i) / 200;
var sampleVal = (short)(Math.Sin( i * angularFreq ) * 28000.0 * envelope * clampedVol);
pcmSamples[i] = sampleVal;
}
}
// Feed into SoundStream
var stream = new SoundStream( clampedRate, 1 );
stream.WriteData( pcmSamples.AsSpan() );
stream.Play( clampedVol, pitch );
return ToolOutcome<object>.Ready( new
{
Action = "inject_voice_stream",
Mode = normMode,
Channel = channel,
SampleRate = clampedRate,
Duration = clampedDuration,
SampleCount = pcmSamples.Length,
Volume = clampedVol,
Pitch = pitch,
IsMuted = mute,
Status = "Voice stream audio played successfully"
} );
}
catch ( Exception ex )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "audio.stream_inject_failed", "Voice stream injection failed.", ex );
}
}
public static ToolOutcome<object> ProbeSpatialAudio(
string target,
string? soundEventOrFile = null,
float volume = 1.0f,
float maxDistance = 1000f,
bool testOcclusion = true,
string? listenerTarget = null )
{
var scene = ResolveScene();
if ( scene == null )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "audio.no_scene", "No active scene loaded." );
}
var sourceGo = scene.Directory.FindByGuid( Guid.TryParse( target, out var guid ) ? guid : Guid.Empty )
?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, target, StringComparison.OrdinalIgnoreCase ) );
if ( sourceGo == null )
{
return ToolOutcome<object>.Failure( new { Target = target }, ToolStatus.Degraded, "audio.source_not_found", $"Target '{target}' not found." );
}
var listenerGo = !string.IsNullOrWhiteSpace( listenerTarget )
? (scene.Directory.FindByGuid( Guid.TryParse( listenerTarget, out var lg ) ? lg : Guid.Empty ) ?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, listenerTarget, StringComparison.OrdinalIgnoreCase ) ))
: scene.GetAllObjects( false ).FirstOrDefault( o => o.GetComponent<CameraComponent>() != null );
var sourcePos = sourceGo.WorldPosition;
var listenerPos = listenerGo?.WorldPosition ?? Vector3.Zero;
var distance = Vector3.DistanceBetween( sourcePos, listenerPos );
// Check wall / geometry obstruction between source and listener
var trace = scene.Trace.Ray( sourcePos, listenerPos )
.WithoutTags( "trigger" )
.Run();
var isObstructed = trace.Hit && trace.Distance < (distance - 5f);
var calculatedAttenuation = Math.Clamp( 1.0f - (distance / Math.Max( maxDistance, 10f )), 0f, 1.0f );
if ( isObstructed && testOcclusion )
{
calculatedAttenuation *= 0.35f; // Muffled by intervening geometry
}
// Spawn temporary spatial sound emitter on target
var soundEmitter = sourceGo.Components.GetOrCreate<SoundPointComponent>();
soundEmitter.Volume = volume;
soundEmitter.Distance = maxDistance;
soundEmitter.OcclusionEnabled = testOcclusion;
if ( !string.IsNullOrWhiteSpace( soundEventOrFile ) )
{
Sound.Play( soundEventOrFile, sourcePos, volume );
}
else
{
soundEmitter.StartSound();
}
// Inspect voice and visemes if active on source
var voiceComp = sourceGo.GetComponent<Voice>();
return ToolOutcome<object>.Ready( new
{
Action = "spatial_audio_probe",
Source = sourceGo.Name,
SourcePosition = sourcePos,
Listener = listenerGo?.Name ?? "MainCamera",
ListenerPosition = listenerPos,
Distance = distance,
MaxDistance = maxDistance,
IsObstructed = isObstructed,
ObstructionCollider = trace.GameObject?.Name,
EstimatedVolumeAttenuation = calculatedAttenuation,
VoiceComponentPresent = voiceComp != null,
VoiceAmplitude = voiceComp?.Amplitude ?? 0f,
LaughterScore = voiceComp?.LaughterScore ?? 0f,
VisemePhonemeCount = voiceComp?.Visemes?.Count ?? 0,
Status = "Spatial audio probe verified"
} );
}
public static ToolOutcome<object> SimulateChat(
string action = "say",
string message = "Hello from MCP test harness",
string channel = "all",
string? filterWord = null )
{
try
{
var normAction = action.ToLowerInvariant().Trim();
if ( normAction == "say" )
{
// Send via platform chat and console fallback
Sandbox.Platform.Chat.Say( message );
ConsoleSystem.Run( $"say \"{message.Replace( "\"", "\\\"" )}\"" );
return ToolOutcome<object>.Ready( new
{
Action = "say",
Channel = channel,
Message = message,
Sent = true,
PlatformChatEnabled = Sandbox.Platform.Chat.Enabled
} );
}
if ( normAction == "announcement" || normAction == "add_text" )
{
Sandbox.Platform.Chat.AddText( message );
return ToolOutcome<object>.Ready( new
{
Action = "announcement",
Message = message,
Broadcast = true
} );
}
if ( normAction == "test_filter" )
{
var blockedWords = new[] { "cheat", "hack", "exploit", "toxic", "swear" };
if ( !string.IsNullOrWhiteSpace( filterWord ) )
{
blockedWords = blockedWords.Append( filterWord.ToLowerInvariant().Trim() ).ToArray();
}
var isFiltered = blockedWords.Any( w => message.Contains( w, StringComparison.OrdinalIgnoreCase ) );
var sanitizedMessage = message;
foreach ( var w in blockedWords )
{
if ( sanitizedMessage.Contains( w, StringComparison.OrdinalIgnoreCase ) )
{
sanitizedMessage = sanitizedMessage.Replace( w, new string( '*', w.Length ), StringComparison.OrdinalIgnoreCase );
}
}
return ToolOutcome<object>.Ready( new
{
Action = "test_filter",
OriginalMessage = message,
IsFiltered = isFiltered,
SanitizedMessage = sanitizedMessage,
WouldSuppress = isFiltered
} );
}
return ToolOutcome<object>.Ready( new
{
Action = normAction,
Message = message,
Status = "Ready"
} );
}
catch ( Exception ex )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "chat.simulate_failed", "Chat simulation failed.", ex );
}
}
}
}