289 lines
8.4 KiB
C#
289 lines
8.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Media;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
|
|
namespace SortVisualizer;
|
|
|
|
public class VisualizerForm : Form
|
|
{
|
|
int[] array = Array.Empty<int>();
|
|
Random rand = new();
|
|
int delay = 10;
|
|
bool sorting = false;
|
|
|
|
ComboBox algoSelector;
|
|
Button startButton, stopButton, shuffleButton;
|
|
TrackBar speedSlider;
|
|
CheckBox soundToggle;
|
|
Label speedLabel, statusLabel;
|
|
|
|
List<ISortAlgorithm> algorithms;
|
|
CancellationTokenSource? cts;
|
|
|
|
Dictionary<int, byte[]> toneCache = new();
|
|
SemaphoreSlim soundSemaphore = new(1, 1); // prevent sound overlap
|
|
|
|
int highlightedIndex1 = -1;
|
|
int highlightedIndex2 = -1;
|
|
|
|
public VisualizerForm()
|
|
{
|
|
Text = "C# Sorting";
|
|
DoubleBuffered = true;
|
|
MinimumSize = new Size(800, 500);
|
|
Width = 1000;
|
|
Height = 600;
|
|
|
|
algorithms = new() { new BubbleSort(), new InsertionSort(), new BogoSort(),
|
|
new MergeSort() , new StoogeSort()
|
|
};
|
|
|
|
// --- Top Control Panel ---
|
|
var topPanel = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Top,
|
|
AutoSize = true,
|
|
WrapContents = true,
|
|
FlowDirection = FlowDirection.LeftToRight,
|
|
Padding = new Padding(5),
|
|
AutoScroll = true
|
|
};
|
|
|
|
algoSelector = new ComboBox { Width = 160, DropDownStyle = ComboBoxStyle.DropDownList };
|
|
algoSelector.DataSource = algorithms;
|
|
algoSelector.DisplayMember = "Name";
|
|
|
|
int buttonHeight = algoSelector.Height;
|
|
|
|
startButton = new Button { Width = 100, Height = buttonHeight, Text = "Start" };
|
|
stopButton = new Button { Width = 100, Height = buttonHeight, Text = "Stop" };
|
|
shuffleButton = new Button { Width = 100, Height = buttonHeight, Text = "Shuffle" };
|
|
|
|
soundToggle = new CheckBox { Width = 100, Height = buttonHeight, Text = "Sound On", Checked = true };
|
|
|
|
speedLabel = new Label { Width = 60, Height = buttonHeight, Text = "Speed: 10", TextAlign = ContentAlignment.MiddleCenter };
|
|
speedSlider = new TrackBar
|
|
{
|
|
Width = 250,
|
|
Minimum = 1,
|
|
Maximum = 100,
|
|
Value = 10,
|
|
TickFrequency = 10,
|
|
SmallChange = 1,
|
|
LargeChange = 10,
|
|
Height = buttonHeight
|
|
};
|
|
|
|
statusLabel = new Label { Width = 150, Height = buttonHeight, Text = "Idle", TextAlign = ContentAlignment.MiddleRight };
|
|
|
|
topPanel.Controls.AddRange(new Control[]
|
|
{
|
|
algoSelector, startButton, stopButton, shuffleButton,
|
|
soundToggle, speedLabel, speedSlider, statusLabel
|
|
});
|
|
|
|
Controls.Add(topPanel);
|
|
|
|
speedSlider.ValueChanged += (s, e) =>
|
|
{
|
|
delay = 101 - speedSlider.Value;
|
|
speedLabel.Text = $"Speed: {speedSlider.Value}";
|
|
};
|
|
|
|
startButton.Click += async (s, e) => await StartSort();
|
|
stopButton.Click += (s, e) => StopSort();
|
|
shuffleButton.Click += (s, e) => Shuffle();
|
|
|
|
this.Resize += (s, e) => Invalidate();
|
|
|
|
Shuffle();
|
|
}
|
|
|
|
void Shuffle()
|
|
{
|
|
if (sorting) return;
|
|
array = Enumerable.Range(1, 100).OrderBy(_ => rand.Next()).ToArray();
|
|
highlightedIndex1 = highlightedIndex2 = -1;
|
|
Invalidate();
|
|
statusLabel.Text = "Shuffled!";
|
|
}
|
|
|
|
async Task StartSort()
|
|
{
|
|
if (sorting) return;
|
|
sorting = true;
|
|
cts = new CancellationTokenSource();
|
|
|
|
var algo = (ISortAlgorithm)algoSelector.SelectedItem!;
|
|
statusLabel.Text = $"Sorting ( {algo.Name} )...";
|
|
|
|
try
|
|
{
|
|
await algo.Sort(
|
|
array,
|
|
() => Invalidate(), // keep original signature
|
|
delay,
|
|
value => { if (soundToggle.Checked) _ = PlaySoundAsync(value); },
|
|
cts.Token
|
|
);
|
|
|
|
highlightedIndex1 = highlightedIndex2 = -1;
|
|
Invalidate();
|
|
|
|
if (!cts.Token.IsCancellationRequested)
|
|
statusLabel.Text = "Done!";
|
|
}
|
|
finally
|
|
{
|
|
sorting = false;
|
|
}
|
|
}
|
|
|
|
void StopSort()
|
|
{
|
|
if (!sorting) return;
|
|
cts?.Cancel();
|
|
sorting = false;
|
|
highlightedIndex1 = highlightedIndex2 = -1;
|
|
Invalidate();
|
|
statusLabel.Text = "Stopped.";
|
|
}
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
base.OnPaint(e);
|
|
var g = e.Graphics;
|
|
|
|
int offsetY = 50;
|
|
int marginX = 20;
|
|
|
|
if (array.Length == 0) return;
|
|
|
|
int barWidth = (ClientSize.Width - 2 * marginX) / array.Length;
|
|
int availableHeight = ClientSize.Height - offsetY;
|
|
|
|
for (int i = 0; i < array.Length; i++)
|
|
{
|
|
int height = (int)(array[i] * (availableHeight / (float)array.Length));
|
|
Brush brush = Brushes.LightGreen;
|
|
|
|
if (sorting)
|
|
{
|
|
if (i == highlightedIndex1) brush = Brushes.Red;
|
|
else if (i == highlightedIndex2) brush = Brushes.Orange;
|
|
else brush = Brushes.DeepSkyBlue;
|
|
}
|
|
|
|
g.FillRectangle(brush, marginX + i * barWidth, ClientSize.Height - height, barWidth - 1, height);
|
|
}
|
|
}
|
|
|
|
async Task PlaySoundAsync(int value)
|
|
{
|
|
// Use semaphore to prevent overlapping sounds
|
|
if (!await soundSemaphore.WaitAsync(0))
|
|
return; // skip if already playing
|
|
|
|
try
|
|
{
|
|
int freq = 220 + (int)(value * 12.0);
|
|
int durationMs = 10;
|
|
|
|
if (!toneCache.TryGetValue(freq, out var wav))
|
|
{
|
|
wav = GenerateToneWav(freq, durationMs, 44100);
|
|
|
|
// Limit cache size
|
|
if (toneCache.Count > 200)
|
|
toneCache.Clear();
|
|
|
|
toneCache[freq] = wav;
|
|
}
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
using var ms = new MemoryStream(wav);
|
|
using var player = new SoundPlayer(ms);
|
|
player.PlaySync(); // blocks on background thread
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
Console.Beep(freq, durationMs);
|
|
}
|
|
catch { }
|
|
}
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
soundSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
cts?.Dispose();
|
|
soundSemaphore?.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
#region Tone Generation
|
|
|
|
public static byte[] GenerateToneWav(int frequency, int durationMs, int sampleRate = 44100, double amplitude = 0.25)
|
|
{
|
|
int samples = (int)((durationMs / 1000.0) * sampleRate);
|
|
using var ms = new MemoryStream();
|
|
using var bw = new BinaryWriter(ms);
|
|
|
|
short numChannels = 1;
|
|
short bitsPerSample = 16;
|
|
int byteRate = sampleRate * numChannels * bitsPerSample / 8;
|
|
short blockAlign = (short)(numChannels * bitsPerSample / 8);
|
|
int dataSize = samples * numChannels * (bitsPerSample / 8);
|
|
|
|
bw.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"));
|
|
bw.Write(36 + dataSize);
|
|
bw.Write(System.Text.Encoding.ASCII.GetBytes("WAVE"));
|
|
|
|
bw.Write(System.Text.Encoding.ASCII.GetBytes("fmt "));
|
|
bw.Write(16);
|
|
bw.Write((short)1);
|
|
bw.Write(numChannels);
|
|
bw.Write(sampleRate);
|
|
bw.Write(byteRate);
|
|
bw.Write(blockAlign);
|
|
bw.Write(bitsPerSample);
|
|
|
|
bw.Write(System.Text.Encoding.ASCII.GetBytes("data"));
|
|
bw.Write(dataSize);
|
|
|
|
double twoPiF = 2 * Math.PI * frequency;
|
|
for (int n = 0; n < samples; n++)
|
|
{
|
|
double t = n / (double)sampleRate;
|
|
double sample = amplitude * Math.Sin(twoPiF * t);
|
|
short s = (short)Math.Max(short.MinValue, Math.Min(short.MaxValue, sample * short.MaxValue));
|
|
bw.Write(s);
|
|
}
|
|
|
|
bw.Flush();
|
|
return ms.ToArray();
|
|
}
|
|
|
|
#endregion
|
|
} |