diff --git a/MainForm.cs b/MainForm.cs index 3ab6c35..741801c 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -52,11 +52,13 @@ public class MainForm : Form readonly Button _btnLive; readonly Button _btnPattern; + readonly Button _btnRandom; readonly Label _lblTrack; readonly Label _lblAudioDev; readonly ComboBox _cboAudioDev; bool _isLiveRunning; bool _isPatternRunning; + bool _isRandomRunning; LiveCapture? _liveCapture; public MainForm(CoyoteDevice device, State state, Server server) @@ -123,31 +125,39 @@ public class MainForm : Form { Text = "Test", Location = new Point(16, 96), - Size = new Size(80, 32) + Size = new Size(68, 32) }; _btnTest.Click += OnTest; _btnLive = new Button { Text = "Live", - Location = new Point(104, 96), - Size = new Size(80, 32) + Location = new Point(92, 96), + Size = new Size(68, 32) }; _btnLive.Click += OnLive; _btnPattern = new Button { Text = "Pattern", - Location = new Point(192, 96), - Size = new Size(80, 32) + Location = new Point(168, 96), + Size = new Size(68, 32) }; _btnPattern.Click += OnPattern; + _btnRandom = new Button + { + Text = "Random", + Location = new Point(244, 96), + Size = new Size(68, 32) + }; + _btnRandom.Click += OnRandom; + _btnStop = new Button { Text = "Stop All", - Location = new Point(280, 96), - Size = new Size(80, 32) + Location = new Point(320, 96), + Size = new Size(76, 32) }; _btnStop.Click += OnStop; @@ -350,7 +360,7 @@ public class MainForm : Form }; OnAmpChanged(null, EventArgs.Empty); - Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnLive, _btnPattern, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB, _lblAmpA, _ampBarA, _lblAmpValA, _lblAmpB, _ampBarB, _lblAmpValB }); + Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _btnTest, _btnLive, _btnPattern, _btnRandom, _btnStop, _lblAudioDev, _cboAudioDev, _lblTrack, _lblSendA, _heatA, _lblSendB, _heatB, _lblRecvA, _gaugeRecvA, _lblRecvB, _gaugeRecvB, _lblLimitA, _limitBarA, _lblLimitValA, _chkScaleA, _lblLimitB, _limitBarB, _lblLimitValB, _chkScaleB, _lblAmpA, _ampBarA, _lblAmpValA, _lblAmpB, _ampBarB, _lblAmpValB }); // Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange _iconNeutral = CreateVoltageIcon(Color.Gray); @@ -520,10 +530,11 @@ public class MainForm : Form void RefreshButtonStates() { - bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning; + bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning || _isRandomRunning; _btnTest.Enabled = !_server.HasClient && !busy; _btnLive.Enabled = !_server.HasClient && !busy; _btnPattern.Enabled = !_server.HasClient && !busy; + _btnRandom.Enabled = !_server.HasClient && !busy; _btnStop.Enabled = true; } @@ -539,6 +550,10 @@ public class MainForm : Form { _lblTrack.Text = $"Pattern: {_patternName}"; } + else if (_isRandomRunning) + { + _lblTrack.Text = "Random noise"; + } UpdateTrayIcon(); } @@ -605,6 +620,7 @@ public class MainForm : Form { StopLive(); StopPattern(); + StopRandom(); _state.SetStrength('A', 0); _state.SetStrength('B', 0); _state.Stop('A'); @@ -772,6 +788,90 @@ public class MainForm : Form } } + CancellationTokenSource? _randomCts; + + void OnRandom(object? sender, EventArgs e) + { + if (_isRandomRunning || _server.HasClient) return; + + _isRandomRunning = true; + _btnRandom.Enabled = false; + _lblTrack.Text = "Random noise"; + + _state.SetStrength('A', 60); + _state.SetStrength('B', 60); + _state.Stop('A'); + _state.Stop('B'); + + _randomCts = new CancellationTokenSource(); + Task.Run(() => RunRandomAsync(_randomCts.Token)); + } + + async Task RunRandomAsync(CancellationToken ct) + { + // Pink noise via Voss-McCartney algorithm (1/f spectrum) + // Separate state per channel, B inverted for counterphase + var rng = new Random(); + int octaves = 8; + var rowsA = new double[octaves]; + var rowsB = new double[octaves]; + double pinkA = 0, pinkB = 0; + + while (!ct.IsCancellationRequested && _isRandomRunning) + { + // Channel A + int updateA = rng.Next(octaves); + rowsA[updateA] = rng.NextDouble() * 2 - 1; + pinkA = 0; + for (int i = 0; i < octaves; i++) pinkA += rowsA[i]; + pinkA /= octaves; + + // Channel B (independent state, inverted for counterphase) + int updateB = rng.Next(octaves); + rowsB[updateB] = rng.NextDouble() * 2 - 1; + pinkB = 0; + for (int i = 0; i < octaves; i++) pinkB += rowsB[i]; + pinkB /= octaves; + pinkB = -pinkB; // counterphase + + // Map pink noise (-1..1) to intensity (0..80, capped for comfort) + int intA = (int)Math.Clamp((pinkA + 1) * 40, 0, 80); + int intB = (int)Math.Clamp((pinkB + 1) * 40, 0, 80); + + // Random frequency: map pink noise to period (10-1000ms) + int freqMsA = (int)Math.Clamp((pinkA + 1) * 500, 10, 1000); + int freqMsB = (int)Math.Clamp((pinkB + 1) * 500, 10, 1000); + + var frameA = new WaveFrame( + Freq.Compress4(new[] { freqMsA, freqMsA, freqMsA, freqMsA }), + Intensity.Clamp4(new[] { intA, intA, intA, intA })); + var frameB = new WaveFrame( + Freq.Compress4(new[] { freqMsB, freqMsB, freqMsB, freqMsB }), + Intensity.Clamp4(new[] { intB, intB, intB, intB })); + + _state.EnqueueStream('A', new[] { frameA }); + _state.EnqueueStream('B', new[] { frameB }); + + try { await Task.Delay(100, ct); } catch (OperationCanceledException) { break; } + } + } + + void StopRandom() + { + _randomCts?.Cancel(); + _randomCts = null; + + if (_isRandomRunning) + { + _isRandomRunning = false; + if (!IsDisposed) + { + _lblTrack.Text = ""; + RefreshButtonStates(); + } + } + } + string ShowInputDialog(string prompt, string title) { using var dlg = new Form