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();
}
}