Editor/AudioWaveForm.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;

namespace Ardi;

public class AudioWaveForm : GraphicsItem
{
	private struct Column
	{
		public float top;
		public float bottom;
	}

	private readonly AudioTimelineView TimelineView;
	private readonly List<Column> Columns = new List<Column>();
	private bool isDirty;

	public short[] Samples { get; private set; }
	public float Duration { get; private set; }
	public int SampleCount => Samples?.Length ?? 0;
	public int SampleRate { get; private set; }

	private short MinSample = short.MaxValue;
	private short MaxSample = short.MinValue;
	private int SamplesPerColumn;
	private const float LineWidth = 1f;
	private float LineSize => 1f;

	public AudioWaveForm( AudioTimelineView view ) : base( null )
	{
		TimelineView = view;
		ZIndex = -1f;
	}

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

		if ( isDirty )
		{
			Analyse();
		}

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

		if ( Columns.Count > 0 )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.Primary );
			var height = LocalRect.Height;

			var startCol = (int)(TimelineView.VisibleRect.Left / LineSize);
			var endCol = (int)(TimelineView.VisibleRect.Right / LineSize);

			for ( int i = startCol; i <= endCol && i <= Columns.Count - 1; i++ )
			{
				var column = Columns[i];
				var top = height * column.top;
				var bottom = height * column.bottom;

				var rect = new Rect(
					new Vector2( i * LineSize, bottom ),
					new Vector2( 1f, Math.Max( 1f, top - bottom ) )
				);
				Paint.DrawRect( rect );
			}
		}

		if ( TimelineView.IsSplitMode )
		{
			Paint.SetPen( Theme.Red );
			foreach ( var point in TimelineView.SplitPoints )
			{
				var x = (float)point / SampleCount * Width;
				if ( x >= TimelineView.VisibleRect.Left && x <= TimelineView.VisibleRect.Right )
				{
					Paint.DrawLine( new Vector2( x, 0 ), new Vector2( x, LocalRect.Height ) );
				}
			}
		}
		else
		{
			Paint.SetPen( Theme.Green );
			foreach ( var point in TimelineView.LoopPoints )
			{
				var x = (float)point / SampleCount * Width;
				if ( x >= TimelineView.VisibleRect.Left && x <= TimelineView.VisibleRect.Right )
				{
					Paint.DrawLine( new Vector2( x, 0 ), new Vector2( x, LocalRect.Height ) );
				}
			}
		}
	}

	public void SetSamples( short[] samples, float duration )
	{
		Samples = samples;
		Duration = duration;
		SampleRate = (int)(samples.Length / duration);
		Width = Duration * LineSize;
		isDirty = true;
	}

	public void Analyse()
	{
		isDirty = false;
		MinSample = short.MaxValue;
		MaxSample = short.MinValue;
		Columns.Clear();

		if ( Samples == null || Samples.Length == 0 ) return;

		var sampleCount = Samples.Length;
		for ( int i = 0; i < sampleCount; i++ )
		{
			var sample = Samples[i];
			MinSample = Math.Min( sample, MinSample );
			MaxSample = Math.Max( sample, MaxSample );
		}

		var maxRange = Math.Max( Math.Abs( MinSample ), Math.Abs( MaxSample ) );
		var minRange = -maxRange;
		var range = minRange - maxRange;

		var columnCount = MathX.FloorToInt( TimelineView.PositionFromTime( TimelineView.Duration ) / LineSize );
		SamplesPerColumn = sampleCount / columnCount;

		for ( int j = 0; j < columnCount - 1; j++ )
		{
			var startIndex = j * SamplesPerColumn;
			var endIndex = (j + 1) * SamplesPerColumn;
			GetAverages( Samples, startIndex, endIndex, out var posAvg, out var negAvg );

			Columns.Add( new Column
			{
				top = range != 0f ? (negAvg - maxRange) / range : 0.5f,
				bottom = range != 0f ? (posAvg - maxRange) / range : 0.5f
			} );
		}

		Update();
	}

	private static void GetAverages( short[] data, int startIndex, int endIndex, out float posAvg, out float negAvg )
	{
		posAvg = 0f;
		negAvg = 0f;
		int posCount = 0;
		int negCount = 0;

		for ( int i = startIndex; i < endIndex && i < data.Length; i++ )
		{
			if ( data[i] > 0 )
			{
				posCount++;
				posAvg += data[i];
			}
			else
			{
				negCount++;
				negAvg += data[i];
			}
		}

		if ( posCount > 0 ) posAvg /= posCount;
		if ( negCount > 0 ) negAvg /= negCount;
	}
}