Editor/AudioTools.cs
using Editor;
using Sandbox;
using System;
using System.IO;

namespace Ardi;

[Dock( "Editor", "Audio Spectral Tool", "audiotrack" )]
public class AudioTools : Widget
{
	private readonly AudioTimelineView Timeline;
	private readonly Option PlayOption; 
	private readonly Option ModeOption;
	private readonly Option AutoLoopOption;
	private readonly Option ResetOption;
	private readonly Option ProcessOption;

	private bool _prevPlay = false;
	private bool isSplitMode = true;

	public bool Playing { get; set; }
	public bool Repeating { get; set; }
	public float Time { get; private set; }
	public ToolBar ToolBar { get; private set; }

	private SoundFile currentSound;
	private short[] audioSamples;
	private int sampleRate;
	private float audioDuration;

	public AudioTools( Widget parent ) : base( parent )
	{
		Name = "Audio Spectral Tool";
		WindowTitle = "Audio Spectral Tool";
		SetWindowIcon( "audiotrack" );
		Size = new Vector2( 900, 600 );
		MinimumSize = new Vector2( 600, 400 );
		AcceptDrops = true;

		Layout = Layout.Column();

		var header = Layout.AddRow();

		ToolBar = header.Add( new ToolBar( this ) );
		ToolBar.NoSystemBackground = false;
		ToolBar.SetIconSize( 18 );

		PlayOption = ToolBar.AddOption( "Play", "play_arrow", () => Playing = !Playing );
		PlayOption.ToolTip = "Play or pause audio (Space)";

		var skipStart = ToolBar.AddOption( "Skip to Start", "skip_previous", () => Timeline?.MoveScrubber( 0 ) );
		skipStart.ToolTip = "Jump to the beginning of the audio";
		ToolBar.AddSeparator();

		var loop = ToolBar.AddOption( "Loop", "repeat" );
		loop.Bind( "Checked" ).From( this, nameof( Repeating ) );
		loop.Checkable = true;
		loop.ToolTip = "Enable looping playback";

		ToolBar.AddSeparator();

		ModeOption = ToolBar.AddOption( "Split Mode", "content_cut", () => ToggleMode() );
		ModeOption.ToolTip = "Toggle between Split and Loop modes";

		AutoLoopOption = ToolBar.AddOption( "Auto Loop", "tune", () => DetectLoopPoints() );
		AutoLoopOption.Enabled = false;
		AutoLoopOption.ToolTip = "Automatically detect optimal loop points";

		ResetOption = ToolBar.AddOption( "Reset", "clear", () => ResetPoints() );
		ResetOption.ToolTip = "Clear all split/loop points";

		ToolBar.AddSeparator();

		ProcessOption = ToolBar.AddOption( "Process & Save", "save", () => ProcessAndSave() );
		ProcessOption.ToolTip = "Export split segments or loop as WAV files";

		ToolBar.AddSeparator();

		var helpOption = ToolBar.AddOption( "Help", "help", () => ShowHelp() );
		helpOption.ToolTip = "Show user guide and keyboard shortcuts";

		var timecode = header.Add( new Label( this ) );
		timecode.Bind( "Text" ).ReadOnly().From( () =>
		{
			if ( Timeline == null ) return "00:00.000 / 00:00.000";
			TimeSpan t = TimeSpan.FromSeconds( Timeline.Time );
			TimeSpan d = TimeSpan.FromSeconds( Timeline.Duration );
			return $"{t.ToString( @"mm\:ss\.fff" )} / {d.ToString( @"mm\:ss\.fff" )}";
		}, null );
		timecode.Alignment = TextFlag.RightCenter;

		Timeline = Layout.Add( new AudioTimelineView( this ), 1 );
		Timeline.AcceptDrops = true;
	}

	public override void OnDragDrop( DragEvent e )
	{
		if ( !e.Data.HasFileOrFolder ) return;

		var filePath = e.Data.FileOrFolder;

		var extension = Path.GetExtension( filePath ).ToLowerInvariant();

		if ( extension != ".sound" && extension != ".wav" && extension != ".mp3" )
		{
			Log.Warning( $"Unsupported file format: {extension}" );
			return;
		}

		var asset = AssetSystem.FindByPath( filePath );
		if ( asset?.AssetType == AssetType.SoundFile )
		{
			var soundFile = SoundFile.Load( asset.Path );
			if ( soundFile != null )
			{
				LoadSound( soundFile );
				return;
			}
		}

		try
		{
			var soundFile = SoundFile.Load( filePath );
			if ( soundFile != null )
			{
				LoadSound( soundFile );
				return;
			}
		}
		catch ( Exception ex )
		{
			Log.Error( $"Could not load the dropped audio file: {ex.Message}" );
		}

		if ( extension == ".wav" )
		{
			try
			{
				var wavData = File.ReadAllBytes( filePath );
				var soundFile = SoundFile.FromWav( filePath, wavData, false );
				if ( soundFile != null )
				{
					LoadSound( soundFile );
					return;
				}
			}
			catch ( Exception ex )
			{
				Log.Error( $"Could not load wav file: {ex.Message}" );
			}
		}

		Log.Warning( $"Could not load file {filePath}" );
	}

	public override void OnDragHover( DragEvent e )
	{
		if ( !e.Data.HasFileOrFolder ) return;

		var filePath = e.Data.FileOrFolder;
		var extension = Path.GetExtension( filePath ).ToLowerInvariant();

		if ( extension == ".sound" || extension == ".wav" || extension == ".mp3" )
		{
			e.Action = DropAction.Link;
		}
	}

	private async void LoadSound( SoundFile soundFile )
	{
		currentSound = soundFile;

		try
		{
			await soundFile.LoadAsync();
			audioSamples = await soundFile.GetSamplesAsync();

			if ( audioSamples == null )
			{
				Log.Error( "Failed to load audio data" );
				return;
			}

			sampleRate = soundFile.Rate;
			audioDuration = soundFile.Duration;

			Timeline.SetSamples( audioSamples, audioDuration, soundFile.ResourcePath, 1 );
			Timeline.ResetPoints();
			Timeline.SetSourceFilePath( soundFile.ResourcePath );

			PlayOption.Enabled = true;
			ResetOption.Enabled = true;
			ProcessOption.Enabled = true;
			AutoLoopOption.Enabled = true;
		}
		catch ( Exception ex )
		{
			Log.Error( $"Error loading audio: {ex.Message}" );
		}
	}

	private void ToggleMode()
	{
		isSplitMode = !isSplitMode;
		Timeline?.SetMode( isSplitMode );

		if ( isSplitMode )
		{
			ModeOption.Text = "Split Mode";
			ModeOption.Icon = "content_cut";
		}
		else
		{
			ModeOption.Text = "Loop Mode";
			ModeOption.Icon = "loop";
		}
	}

	private void DetectLoopPoints()
	{
		Timeline?.DetectLoopPoints();
	}

	private void ResetPoints()
	{
		Timeline?.ResetPoints();
	}

	private void ProcessAndSave()
	{
		if ( currentSound == null || audioSamples == null ) return;

		try
		{
			var baseFileName = Path.GetFileNameWithoutExtension( currentSound.ResourcePath );
			var sourcePath = Path.GetDirectoryName( currentSound.ResourcePath );
			string outputDir = sourcePath;

			if ( string.IsNullOrEmpty( outputDir ) || !Directory.Exists( outputDir ) )
			{
				outputDir = Project.Current.GetAssetsPath();
			}

			Timeline?.ProcessAndSave( baseFileName, outputDir, audioSamples, currentSound );
		}
		catch ( Exception ex )
		{
			Log.Error( $"Error saving: {ex.Message}" );
		}
	}

	private void ShowHelp()
	{
		var helpDialog = new AudioToolsHelpDialog( this );
		helpDialog.Show();
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.Antialiasing = false;
		Paint.ClearPen();
		Paint.SetBrush( Theme.WidgetBackground );
		Paint.DrawRect( LocalRect );
	}

	[EditorEvent.Frame]
	protected void OnFrame()
	{
		Timeline?.OnFrame();
		if ( Timeline != null )
			Time = Timeline.Time;

		PlayOption.Text = Playing ? "Pause" : "Play";
		PlayOption.Icon = Playing ? "pause" : "play_arrow";

		if ( !Editor.Application.FocusWidget.IsValid() ) return;

		if ( Editor.Application.IsKeyDown( KeyCode.Space ) && Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) && !_prevPlay )
		{
			Play( 0 );
		}
		else if ( Editor.Application.IsKeyDown( KeyCode.Space ) && !_prevPlay )
		{
			Playing = !Playing;
		}
		_prevPlay = Editor.Application.IsKeyDown( KeyCode.Space );
	}

	public void Play()
	{
		Playing = true;
	}

	public void Play( float start )
	{
		Timeline?.MoveScrubber( start );
		Play();
	}

	[Menu( "Tools", "Audio Spectral Tool", "audiotrack" )]
	public static void OpenAudioSpectralTool()
	{
		var tool = new AudioTools( null );
		tool.Show();
	}
}