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:
+109
-9
@@ -52,11 +52,13 @@ public class MainForm : Form
|
|||||||
|
|
||||||
readonly Button _btnLive;
|
readonly Button _btnLive;
|
||||||
readonly Button _btnPattern;
|
readonly Button _btnPattern;
|
||||||
|
readonly Button _btnRandom;
|
||||||
readonly Label _lblTrack;
|
readonly Label _lblTrack;
|
||||||
readonly Label _lblAudioDev;
|
readonly Label _lblAudioDev;
|
||||||
readonly ComboBox _cboAudioDev;
|
readonly ComboBox _cboAudioDev;
|
||||||
bool _isLiveRunning;
|
bool _isLiveRunning;
|
||||||
bool _isPatternRunning;
|
bool _isPatternRunning;
|
||||||
|
bool _isRandomRunning;
|
||||||
LiveCapture? _liveCapture;
|
LiveCapture? _liveCapture;
|
||||||
|
|
||||||
public MainForm(CoyoteDevice device, State state, Server server)
|
public MainForm(CoyoteDevice device, State state, Server server)
|
||||||
@@ -123,31 +125,39 @@ public class MainForm : Form
|
|||||||
{
|
{
|
||||||
Text = "Test",
|
Text = "Test",
|
||||||
Location = new Point(16, 96),
|
Location = new Point(16, 96),
|
||||||
Size = new Size(80, 32)
|
Size = new Size(68, 32)
|
||||||
};
|
};
|
||||||
_btnTest.Click += OnTest;
|
_btnTest.Click += OnTest;
|
||||||
|
|
||||||
_btnLive = new Button
|
_btnLive = new Button
|
||||||
{
|
{
|
||||||
Text = "Live",
|
Text = "Live",
|
||||||
Location = new Point(104, 96),
|
Location = new Point(92, 96),
|
||||||
Size = new Size(80, 32)
|
Size = new Size(68, 32)
|
||||||
};
|
};
|
||||||
_btnLive.Click += OnLive;
|
_btnLive.Click += OnLive;
|
||||||
|
|
||||||
_btnPattern = new Button
|
_btnPattern = new Button
|
||||||
{
|
{
|
||||||
Text = "Pattern",
|
Text = "Pattern",
|
||||||
Location = new Point(192, 96),
|
Location = new Point(168, 96),
|
||||||
Size = new Size(80, 32)
|
Size = new Size(68, 32)
|
||||||
};
|
};
|
||||||
_btnPattern.Click += OnPattern;
|
_btnPattern.Click += OnPattern;
|
||||||
|
|
||||||
|
_btnRandom = new Button
|
||||||
|
{
|
||||||
|
Text = "Random",
|
||||||
|
Location = new Point(244, 96),
|
||||||
|
Size = new Size(68, 32)
|
||||||
|
};
|
||||||
|
_btnRandom.Click += OnRandom;
|
||||||
|
|
||||||
_btnStop = new Button
|
_btnStop = new Button
|
||||||
{
|
{
|
||||||
Text = "Stop All",
|
Text = "Stop All",
|
||||||
Location = new Point(280, 96),
|
Location = new Point(320, 96),
|
||||||
Size = new Size(80, 32)
|
Size = new Size(76, 32)
|
||||||
};
|
};
|
||||||
_btnStop.Click += OnStop;
|
_btnStop.Click += OnStop;
|
||||||
|
|
||||||
@@ -350,7 +360,7 @@ public class MainForm : Form
|
|||||||
};
|
};
|
||||||
OnAmpChanged(null, EventArgs.Empty);
|
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
|
// Tray icon — three cached variants: neutral=gray, active=gold, hot=red-orange
|
||||||
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
_iconNeutral = CreateVoltageIcon(Color.Gray);
|
||||||
@@ -520,10 +530,11 @@ public class MainForm : Form
|
|||||||
|
|
||||||
void RefreshButtonStates()
|
void RefreshButtonStates()
|
||||||
{
|
{
|
||||||
bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning;
|
bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning || _isRandomRunning;
|
||||||
_btnTest.Enabled = !_server.HasClient && !busy;
|
_btnTest.Enabled = !_server.HasClient && !busy;
|
||||||
_btnLive.Enabled = !_server.HasClient && !busy;
|
_btnLive.Enabled = !_server.HasClient && !busy;
|
||||||
_btnPattern.Enabled = !_server.HasClient && !busy;
|
_btnPattern.Enabled = !_server.HasClient && !busy;
|
||||||
|
_btnRandom.Enabled = !_server.HasClient && !busy;
|
||||||
_btnStop.Enabled = true;
|
_btnStop.Enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +550,10 @@ public class MainForm : Form
|
|||||||
{
|
{
|
||||||
_lblTrack.Text = $"Pattern: {_patternName}";
|
_lblTrack.Text = $"Pattern: {_patternName}";
|
||||||
}
|
}
|
||||||
|
else if (_isRandomRunning)
|
||||||
|
{
|
||||||
|
_lblTrack.Text = "Random noise";
|
||||||
|
}
|
||||||
|
|
||||||
UpdateTrayIcon();
|
UpdateTrayIcon();
|
||||||
}
|
}
|
||||||
@@ -605,6 +620,7 @@ public class MainForm : Form
|
|||||||
{
|
{
|
||||||
StopLive();
|
StopLive();
|
||||||
StopPattern();
|
StopPattern();
|
||||||
|
StopRandom();
|
||||||
_state.SetStrength('A', 0);
|
_state.SetStrength('A', 0);
|
||||||
_state.SetStrength('B', 0);
|
_state.SetStrength('B', 0);
|
||||||
_state.Stop('A');
|
_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)
|
string ShowInputDialog(string prompt, string title)
|
||||||
{
|
{
|
||||||
using var dlg = new Form
|
using var dlg = new Form
|
||||||
|
|||||||
Reference in New Issue
Block a user