Editor/SoundAddons.cs
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Reflection;
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace Editor;

public static class SoundAddons
{
	private static string previousSoundGenerationPromp;
	private static string previousSoundGenerationDuration;

	// [ContextMenuFor( typeof( SoundFile ), "Generate sound", "create" )]
	public static void Generate( SoundEvent soundEvent, Action<SoundFile> finished )
	{
		string defaultInput = !string.IsNullOrEmpty( previousSoundGenerationPromp )
			? previousSoundGenerationPromp
			: "A dog barking";

		Dialog.AskString( ( description ) =>
		{
			previousSoundGenerationPromp = description;

			Dialog.AskString( async ( durationStr ) =>
			{
				previousSoundGenerationDuration = durationStr;
				if ( !float.TryParse( durationStr, out float duration ) || duration < 0.5f || duration > 22f )
				{
					Log.Error( "Duration must be between 0.5 and 22 seconds" );
					return;
				}

				try
				{
					string apiKey = SoundSettings.Settings.ElevenLabsApiKey;
					var client = new HttpClient();
					client.DefaultRequestHeaders.Add( "xi-api-key", apiKey );

					var content = new StringContent( JsonSerializer.Serialize( new
					{
						text = description,
						duration_seconds = duration,
						prompt_influence = 0.3
					} ), Encoding.UTF8, "application/json" );

					var response = await client.PostAsync(
						"https://api.elevenlabs.io/v1/sound-generation",
						content
					);

					if ( response.IsSuccessStatusCode )
					{
						var bytes = await response.Content.ReadAsByteArrayAsync();

						string safeFileName = description.Replace( " ", "_" ).Replace( "/", "_" ).Replace( "\\", "_" );
						var mp3Path = Path.Combine(
							SoundSettings.Settings.GenerationPath,
							$"{safeFileName}_{DateTime.Now:yyyyMMddHHmmss}.mp3"
						);

						var fullMp3Path = Path.Combine( Project.Current.GetAssetsPath(), mp3Path );
						Directory.CreateDirectory( Path.GetDirectoryName( fullMp3Path ) );

						await File.WriteAllBytesAsync( fullMp3Path, bytes );
						Log.Info( "File saved to: " + fullMp3Path );

						var mp3Asset = AssetSystem.RegisterFile( fullMp3Path );
						if ( mp3Asset != null )
						{
							var soundFile = SoundFile.Load( mp3Asset.RelativePath );
							if ( soundFile != null )
							{
								finished(soundFile);
								Log.Info( $"Successfully generated and linked sound: {mp3Asset.RelativePath}" );
							}
							else
							{
								Log.Error( "Failed to load generated sound file" );
							}
						}
						else
						{
							Log.Error( "Failed to register generated mp3 file as asset" );
						}
					}
					else
					{
						var errorContent = await response.Content.ReadAsStringAsync();
						Log.Error( $"Failed to generate sound: {response.StatusCode}. Error: {errorContent}" );
					}
				}
				catch ( Exception ex )
				{
					Log.Error( $"Error generating sound: {ex.Message}" );
				}
			},
			"Enter duration in seconds (0.5 to 22):",
			"Generate",
			"Cancel",
			previousSoundGenerationDuration,
			"Set Sound Duration" );
		},
		"Describe the sound effect to generate:",
		"Next",
		"Cancel",
		defaultInput,
		"Generate Sound Effect" );
	}

	// [ContextMenuFor( typeof( SoundFile ), "Split sound", "content_cut" )]
	public static async void SplitSound( Widget parent, SerializedProperty property )
	{
		var soundResource = property.GetValue<SoundFile>();
		if ( soundResource == null || !soundResource.IsValid )
		{
			Log.Error( "No valid sound file selected" );
			return;
		}

		var dialog = new Dialog();
		dialog.Window.Title = "Split Sound";
		dialog.Window.Size = new Vector2( 800, 400 );

		dialog.Layout = Layout.Column();
		var mainLayout = dialog.Layout.AddColumn();
		mainLayout.Margin = 16f;

		var loadingLabel = new Label( "Loading audio data..." );
		mainLayout.Add( loadingLabel );

		dialog.Show();

		var samples = await soundResource.GetSamplesAsync();
		if ( samples == null )
		{
			loadingLabel.Text = "Failed to load audio samples";
			return;
		}

		loadingLabel.Destroy();

		var spectogramWidget = new SpectogramWidget( soundResource );
		mainLayout.Add( spectogramWidget, 1 );

		var controlsLayout = mainLayout.AddRow();

		var splitButton = new Button.Primary( "Split", "content_cut" );
		controlsLayout.Add( splitButton );

		splitButton.Clicked = () =>
		{
			var splitPoints = spectogramWidget.GetSplitPoints();
			if ( splitPoints.Count < 1 ) return;

			var listControlWidget = parent.GetAncestor<ListControlWidget>();
			if ( listControlWidget == null )
			{
				Log.Error( "Could not find ListControlWidget parent" );
				return;
			}

			try
			{
				var baseFileName = Path.GetFileNameWithoutExtension( soundResource.ResourcePath );
				var outputDir = Path.Combine(
					Project.Current.GetAssetsPath(),
					"generated",
					$"{baseFileName}_splits"
				);
				Directory.CreateDirectory( outputDir );

				var newSoundFiles = new List<SoundFile>();

				for ( int i = 0; i < splitPoints.Count - 1; i++ )
				{
					var start = splitPoints[i];
					var end = splitPoints[i + 1];
					var length = end - start;

					var segmentSamples = new short[length];
					Array.Copy( samples, start, segmentSamples, 0, length );

					var wavPath = Path.Combine( outputDir, $"{baseFileName}_part_{i + 1}.wav" );

					using ( var writer = new BinaryWriter( File.Create( wavPath ) ) )
					{
						writer.Write( Encoding.ASCII.GetBytes( "RIFF" ) );
						writer.Write( 36 + (segmentSamples.Length * 2) );
						writer.Write( Encoding.ASCII.GetBytes( "WAVE" ) );

						writer.Write( Encoding.ASCII.GetBytes( "fmt " ) );
						writer.Write( 16 );
						writer.Write( (short)1 );
						writer.Write( (short)soundResource.Channels );
						writer.Write( soundResource.Rate );
						writer.Write( soundResource.Rate * soundResource.Channels * 2 );
						writer.Write( (short)(soundResource.Channels * 2) );
						writer.Write( (short)16 );

						writer.Write( Encoding.ASCII.GetBytes( "data" ) );
						writer.Write( segmentSamples.Length * 2 );

						foreach ( var sample in segmentSamples )
						{
							writer.Write( sample );
						}
					}

					var asset = AssetSystem.RegisterFile( wavPath );
					if ( asset != null )
					{
						var soundFile = SoundFile.Load( asset.RelativePath );
						if ( soundFile != null )
						{
							newSoundFiles.Add( soundFile );
						}
					}
				}

				if ( newSoundFiles.Count > 0 )
				{
					try
					{
						var collectionField = listControlWidget.GetType()
							.GetFields( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )
							.FirstOrDefault( f => typeof( SerializedCollection ).IsAssignableFrom( f.FieldType ) );

						if ( collectionField == null )
						{
							return;
						}

						var collection = collectionField.GetValue( listControlWidget ) as SerializedCollection;
						if ( collection == null )
						{
							return;
						}

						if ( property.TryGetAsObject( out var original ) )
						{
							foreach ( var soundFile in newSoundFiles )
							{
								collection.Add( soundFile );
							}

							collection.Remove( property );
						}
					}
					catch ( Exception ex )
					{
						Log.Error( $"Error manipulating collection: {ex.Message}" );
						Log.Error( $"Stack trace: {ex.StackTrace}" );
					}
				}

				dialog.Close();
			}
			catch ( Exception ex )
			{
				Log.Error( $"Error splitting sound: {ex.Message}" );
			}
		};
	}
}