Add Random button (pink noise generator) with counterphase channels

- Voss-McCartney pink noise (1/f spectrum) for intensity and frequency
- Separate state per channel, channel B inverted for counterphase
- Intensity capped at 80 for comfort, frequency spans full 10-1000ms range
- Runs continuously, stopped via Stop All or switching modes
- 5th button (68px), all buttons resized to fit one row
This commit is contained in:
2026-08-09 12:01:51 +00:00
parent 4e36580b22
commit f1e7d976ca
+109 -9
View File
@@ -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