Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| badc599dee | |||
| 5efdd63212 | |||
| f1e7d976ca | |||
| 4e36580b22 | |||
| 4ad85f57ec | |||
| 4c44d48a02 |
+146
@@ -0,0 +1,146 @@
|
||||
namespace Substation;
|
||||
|
||||
public class DrumDetector
|
||||
{
|
||||
const int HistorySize = 50;
|
||||
|
||||
readonly double[] _lowFluxHistory = new double[HistorySize];
|
||||
readonly double[] _midFluxHistory = new double[HistorySize];
|
||||
readonly double[] _highFluxHistory = new double[HistorySize];
|
||||
int _historyIndex;
|
||||
|
||||
double[]? _prevLowMag;
|
||||
double[]? _prevMidMag;
|
||||
double[]? _prevHighMag;
|
||||
|
||||
readonly bool[] _subKick = new bool[4];
|
||||
readonly bool[] _subSnare = new bool[4];
|
||||
readonly bool[] _subBrass = new bool[4];
|
||||
|
||||
static readonly byte[] FreqKick = Freq.Compress4(new[] { 150, 150, 150, 150 });
|
||||
static readonly byte[] FreqSnare = Freq.Compress4(new[] { 50, 50, 50, 50 });
|
||||
static readonly byte[] FreqBrass = Freq.Compress4(new[] { 10, 10, 10, 10 });
|
||||
static readonly byte[] FreqSilent = Freq.Compress4(new[] { 10, 10, 10, 10 });
|
||||
|
||||
public void ProcessWindow(double[] magnitude, int sampleRate, int windowSize, int subTickIndex)
|
||||
{
|
||||
if (subTickIndex < 0 || subTickIndex > 3) return;
|
||||
|
||||
double binWidth = (double)sampleRate / windowSize;
|
||||
int lowLo = (int)(20 / binWidth), lowHi = (int)(150 / binWidth);
|
||||
int midLo = (int)(200 / binWidth), midHi = (int)(500 / binWidth);
|
||||
int highLo = (int)(5000 / binWidth), highHi = (int)(15000 / binWidth);
|
||||
|
||||
var lowMag = ExtractBand(magnitude, lowLo, lowHi);
|
||||
var midMag = ExtractBand(magnitude, midLo, midHi);
|
||||
var highMag = ExtractBand(magnitude, highLo, highHi);
|
||||
|
||||
double lowFlux = ComputeFlux(lowMag, _prevLowMag);
|
||||
double midFlux = ComputeFlux(midMag, _prevMidMag);
|
||||
double highFlux = ComputeFlux(highMag, _prevHighMag);
|
||||
|
||||
_prevLowMag = lowMag;
|
||||
_prevMidMag = midMag;
|
||||
_prevHighMag = highMag;
|
||||
|
||||
// Update history and compute adaptive thresholds
|
||||
_lowFluxHistory[_historyIndex] = lowFlux;
|
||||
_midFluxHistory[_historyIndex] = midFlux;
|
||||
_highFluxHistory[_historyIndex] = highFlux;
|
||||
_historyIndex = (_historyIndex + 1) % HistorySize;
|
||||
|
||||
double lowThresh = AdaptiveThreshold(_lowFluxHistory);
|
||||
double midThresh = AdaptiveThreshold(_midFluxHistory);
|
||||
double highThresh = AdaptiveThreshold(_highFluxHistory);
|
||||
|
||||
// Detect onsets
|
||||
bool kickOnset = lowFlux > lowThresh && lowFlux > 0.001;
|
||||
bool snareOnset = midFlux > midThresh && highFlux > highThresh && midFlux > 0.001;
|
||||
bool brassOnset = highFlux > highThresh && !snareOnset && highFlux > 0.001;
|
||||
|
||||
if (kickOnset) _subKick[subTickIndex] = true;
|
||||
if (snareOnset) _subSnare[subTickIndex] = true;
|
||||
if (brassOnset) _subBrass[subTickIndex] = true;
|
||||
}
|
||||
|
||||
public WaveFrame BuildFrame()
|
||||
{
|
||||
var intensity = new byte[4];
|
||||
var freqBytes = new byte[16]; // 4 sub-ticks × 4 bytes (but we use per-sub-tick freq)
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
byte freq;
|
||||
if (_subKick[i])
|
||||
{
|
||||
intensity[i] = 100;
|
||||
freq = Freq.Compress(150);
|
||||
}
|
||||
else if (_subSnare[i])
|
||||
{
|
||||
intensity[i] = 100;
|
||||
freq = Freq.Compress(50);
|
||||
}
|
||||
else if (_subBrass[i])
|
||||
{
|
||||
intensity[i] = 100;
|
||||
freq = Freq.Compress(10);
|
||||
}
|
||||
else
|
||||
{
|
||||
intensity[i] = 0;
|
||||
freq = 10;
|
||||
}
|
||||
|
||||
freqBytes[i] = freq;
|
||||
}
|
||||
|
||||
// Reset for next tick
|
||||
Array.Clear(_subKick, 0, 4);
|
||||
Array.Clear(_subSnare, 0, 4);
|
||||
Array.Clear(_subBrass, 0, 4);
|
||||
|
||||
return new WaveFrame(freqBytes, intensity);
|
||||
}
|
||||
|
||||
static double[] ExtractBand(double[] magnitude, int loBin, int hiBin)
|
||||
{
|
||||
if (hiBin <= loBin || hiBin >= magnitude.Length)
|
||||
return Array.Empty<double>();
|
||||
|
||||
var band = new double[hiBin - loBin + 1];
|
||||
Array.Copy(magnitude, loBin, band, 0, band.Length);
|
||||
return band;
|
||||
}
|
||||
|
||||
static double ComputeFlux(double[] current, double[]? previous)
|
||||
{
|
||||
if (previous == null || current.Length != previous.Length) return 0;
|
||||
|
||||
double flux = 0;
|
||||
for (int i = 0; i < current.Length; i++)
|
||||
{
|
||||
double diff = current[i] - previous[i];
|
||||
if (diff > 0) flux += diff;
|
||||
}
|
||||
return flux;
|
||||
}
|
||||
|
||||
static double AdaptiveThreshold(double[] history)
|
||||
{
|
||||
double sum = 0, sumSq = 0;
|
||||
int count = 0;
|
||||
foreach (var v in history)
|
||||
{
|
||||
if (v <= 0) continue;
|
||||
sum += v;
|
||||
sumSq += v * v;
|
||||
count++;
|
||||
}
|
||||
if (count == 0) return double.MaxValue;
|
||||
double mean = sum / count;
|
||||
double variance = sumSq / count - mean * mean;
|
||||
double stddev = variance > 0 ? Math.Sqrt(variance) : 0;
|
||||
return mean + 2 * stddev;
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -22,6 +22,7 @@ public class LiveCapture : IDisposable
|
||||
int _sampleRate;
|
||||
int _fftsPerTick;
|
||||
int _fftIndexInTick;
|
||||
readonly DrumDetector _drums = new();
|
||||
|
||||
public event Action? Stopped;
|
||||
|
||||
@@ -139,7 +140,12 @@ public class LiveCapture : IDisposable
|
||||
_liveMaxFlux = Math.Max(rhythmFlux, _liveMaxFlux * 0.999);
|
||||
_liveMaxMelodyEnergy = Math.Max(melodyEnergy, _liveMaxMelodyEnergy * 0.999);
|
||||
|
||||
tf.RhythmFlux = Math.Max(tf.RhythmFlux, rhythmFlux);
|
||||
// Drum detection: map this FFT window to a sub-tick (0-3)
|
||||
int subTick = _fftsPerTick > 0 ? _fftIndexInTick * 4 / _fftsPerTick : 0;
|
||||
if (subTick > 3) subTick = 3;
|
||||
_drums.ProcessWindow(mag, _sampleRate, MusicAnalyzer.WindowSize, subTick);
|
||||
|
||||
// Melody accumulation for chB
|
||||
tf.MelodyEnergy += melodyEnergy;
|
||||
tf.MelodyFreqSamples.Add(melodyFreq);
|
||||
tf.MelodyCount++;
|
||||
@@ -147,7 +153,8 @@ public class LiveCapture : IDisposable
|
||||
|
||||
if (_fftIndexInTick >= _fftsPerTick)
|
||||
{
|
||||
var (frameA, frameB) = MusicAnalyzer.BuildWaveFrame(tf, _liveMaxFlux, _liveMaxMelodyEnergy);
|
||||
var frameA = _drums.BuildFrame();
|
||||
var frameB = MusicAnalyzer.BuildMelodyFrame(tf, _liveMaxMelodyEnergy);
|
||||
_state.EnqueueStream('A', new[] { frameA });
|
||||
_state.EnqueueStream('B', new[] { frameB });
|
||||
|
||||
|
||||
+186
-85
@@ -16,13 +16,10 @@ public class MainForm : Form
|
||||
readonly Label _lblDeviceName;
|
||||
readonly TextBox _txtDeviceName;
|
||||
readonly Button _btnConnect;
|
||||
readonly Button _btnTest;
|
||||
readonly Button _btnStop;
|
||||
readonly System.Windows.Forms.Timer _statusTimer;
|
||||
readonly CancellationTokenSource _loopCts;
|
||||
|
||||
bool _closingFromTray;
|
||||
|
||||
readonly HeatMap _heatA;
|
||||
readonly HeatMap _heatB;
|
||||
readonly ProgressBar _gaugeRecvA;
|
||||
@@ -33,10 +30,16 @@ public class MainForm : Form
|
||||
readonly Label _lblRecvB;
|
||||
readonly TrackBar _limitBarA;
|
||||
readonly TrackBar _limitBarB;
|
||||
readonly TrackBar _ampBarA;
|
||||
readonly TrackBar _ampBarB;
|
||||
readonly Label _lblLimitA;
|
||||
readonly Label _lblLimitB;
|
||||
readonly Label _lblLimitValA;
|
||||
readonly Label _lblLimitValB;
|
||||
readonly Label _lblAmpA;
|
||||
readonly Label _lblAmpB;
|
||||
readonly Label _lblAmpValA;
|
||||
readonly Label _lblAmpValB;
|
||||
readonly CheckBox _chkScaleA;
|
||||
readonly CheckBox _chkScaleB;
|
||||
readonly CheckBox _chkSwap;
|
||||
@@ -48,11 +51,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)
|
||||
@@ -63,7 +68,7 @@ public class MainForm : Form
|
||||
_loopCts = new CancellationTokenSource();
|
||||
|
||||
Text = $"Substation {typeof(MainForm).Assembly.GetName().Version}";
|
||||
ClientSize = new Size(420, 450);
|
||||
ClientSize = new Size(420, 520);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
@@ -115,18 +120,10 @@ public class MainForm : Form
|
||||
};
|
||||
_chkSwap.CheckedChanged += OnSwapChanged;
|
||||
|
||||
_btnTest = new Button
|
||||
{
|
||||
Text = "Test",
|
||||
Location = new Point(16, 96),
|
||||
Size = new Size(80, 32)
|
||||
};
|
||||
_btnTest.Click += OnTest;
|
||||
|
||||
_btnLive = new Button
|
||||
{
|
||||
Text = "Live",
|
||||
Location = new Point(104, 96),
|
||||
Location = new Point(16, 96),
|
||||
Size = new Size(80, 32)
|
||||
};
|
||||
_btnLive.Click += OnLive;
|
||||
@@ -134,15 +131,23 @@ public class MainForm : Form
|
||||
_btnPattern = new Button
|
||||
{
|
||||
Text = "Pattern",
|
||||
Location = new Point(192, 96),
|
||||
Location = new Point(104, 96),
|
||||
Size = new Size(80, 32)
|
||||
};
|
||||
_btnPattern.Click += OnPattern;
|
||||
|
||||
_btnRandom = new Button
|
||||
{
|
||||
Text = "Random",
|
||||
Location = new Point(192, 96),
|
||||
Size = new Size(80, 32)
|
||||
};
|
||||
_btnRandom.Click += OnRandom;
|
||||
|
||||
_btnStop = new Button
|
||||
{
|
||||
Text = "Stop All",
|
||||
Location = new Point(280, 96),
|
||||
Location = new Point(324, 96),
|
||||
Size = new Size(80, 32)
|
||||
};
|
||||
_btnStop.Click += OnStop;
|
||||
@@ -293,7 +298,60 @@ public class MainForm : Form
|
||||
_chkScaleB.CheckedChanged += OnLimitChanged;
|
||||
OnLimitChanged(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 = new Label
|
||||
{
|
||||
Text = "Amp A",
|
||||
Location = new Point(16, 394),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_ampBarA = new TrackBar
|
||||
{
|
||||
Location = new Point(68, 390),
|
||||
Size = new Size(212, 45),
|
||||
Minimum = 0,
|
||||
Maximum = 100,
|
||||
TickFrequency = 25,
|
||||
Value = 0
|
||||
};
|
||||
_ampBarA.ValueChanged += OnAmpChanged;
|
||||
FixTrackbarKeys(_ampBarA);
|
||||
_lblAmpValA = new Label
|
||||
{
|
||||
Text = "0",
|
||||
Location = new Point(286, 394),
|
||||
Size = new Size(32, 20),
|
||||
TextAlign = ContentAlignment.MiddleRight
|
||||
};
|
||||
|
||||
_lblAmpB = new Label
|
||||
{
|
||||
Text = "Amp B",
|
||||
Location = new Point(16, 442),
|
||||
Size = new Size(48, 20),
|
||||
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
|
||||
};
|
||||
_ampBarB = new TrackBar
|
||||
{
|
||||
Location = new Point(68, 438),
|
||||
Size = new Size(212, 45),
|
||||
Minimum = 0,
|
||||
Maximum = 100,
|
||||
TickFrequency = 25,
|
||||
Value = 0
|
||||
};
|
||||
_ampBarB.ValueChanged += OnAmpChanged;
|
||||
FixTrackbarKeys(_ampBarB);
|
||||
_lblAmpValB = new Label
|
||||
{
|
||||
Text = "0",
|
||||
Location = new Point(286, 442),
|
||||
Size = new Size(32, 20),
|
||||
TextAlign = ContentAlignment.MiddleRight
|
||||
};
|
||||
OnAmpChanged(null, EventArgs.Empty);
|
||||
|
||||
Controls.AddRange(new Control[] { _lblBle, _lblWs, _lblDeviceName, _txtDeviceName, _btnConnect, _chkSwap, _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);
|
||||
@@ -355,8 +413,17 @@ public class MainForm : Form
|
||||
{
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
_heatA.AddTick(freqA, intA, _state.LastActiveA);
|
||||
_heatB.AddTick(freqB, intB, _state.LastActiveB);
|
||||
double aScale = _state.LimitScaleA;
|
||||
double bScale = _state.LimitScaleB;
|
||||
var scaledIntA = new byte[4];
|
||||
var scaledIntB = new byte[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
scaledIntA[i] = (byte)Math.Round(intA[i] * aScale);
|
||||
scaledIntB[i] = (byte)Math.Round(intB[i] * bScale);
|
||||
}
|
||||
_heatA.AddTick(freqA, scaledIntA, _state.LastActiveA);
|
||||
_heatB.AddTick(freqB, scaledIntB, _state.LastActiveB);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -377,7 +444,6 @@ public class MainForm : Form
|
||||
|
||||
void ExitFromTray()
|
||||
{
|
||||
_closingFromTray = true;
|
||||
_tray.Visible = false;
|
||||
Application.Exit();
|
||||
}
|
||||
@@ -390,12 +456,6 @@ public class MainForm : Form
|
||||
|
||||
void OnFormClosing(object? sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (e.CloseReason == CloseReason.UserClosing && !_closingFromTray)
|
||||
{
|
||||
e.Cancel = true;
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
_tray.Visible = false;
|
||||
_statusTimer.Stop();
|
||||
_loopCts.Cancel();
|
||||
@@ -461,10 +521,10 @@ public class MainForm : Form
|
||||
|
||||
void RefreshButtonStates()
|
||||
{
|
||||
bool busy = IsTestRunning || _isLiveRunning || _isPatternRunning;
|
||||
_btnTest.Enabled = !_server.HasClient && !busy;
|
||||
bool busy = _isLiveRunning || _isPatternRunning || _isRandomRunning;
|
||||
_btnLive.Enabled = !_server.HasClient && !busy;
|
||||
_btnPattern.Enabled = !_server.HasClient && !busy;
|
||||
_btnRandom.Enabled = !_server.HasClient && !busy;
|
||||
_btnStop.Enabled = true;
|
||||
}
|
||||
|
||||
@@ -480,72 +540,19 @@ public class MainForm : Form
|
||||
{
|
||||
_lblTrack.Text = $"Pattern: {_patternName}";
|
||||
}
|
||||
else if (_isRandomRunning)
|
||||
{
|
||||
_lblTrack.Text = "Random noise";
|
||||
}
|
||||
|
||||
UpdateTrayIcon();
|
||||
}
|
||||
|
||||
bool IsTestRunning;
|
||||
|
||||
void OnTest(object? sender, EventArgs e)
|
||||
{
|
||||
if (IsTestRunning || _server.HasClient) return;
|
||||
IsTestRunning = true;
|
||||
_btnTest.Enabled = false;
|
||||
|
||||
Task.Run(() => RunTestAsync());
|
||||
}
|
||||
|
||||
async Task RunTestAsync()
|
||||
{
|
||||
// Short feel-good pattern over both channels (~3s), gentle swell, A leads B.
|
||||
const int testStrength = 25;
|
||||
|
||||
_state.SetStrength('A', testStrength);
|
||||
_state.SetStrength('B', testStrength);
|
||||
|
||||
// 30 frames x 100ms = 3 seconds. Deep-ish 7Hz pulse with breathing envelope.
|
||||
var framesA = new List<WaveFrame>();
|
||||
var framesB = new List<WaveFrame>();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double t = (double)i / 30;
|
||||
// Swell 0 -> 70 -> 0 over the run
|
||||
double env = Math.Sin(Math.PI * t) * 70;
|
||||
int intA = (int)Math.Round(env);
|
||||
int intB = (int)Math.Round(env * 0.7); // B slightly softer
|
||||
|
||||
int freqMs = 150; // ~7Hz deep
|
||||
framesA.Add(new WaveFrame(
|
||||
Freq.Compress4(new[] { freqMs, freqMs, freqMs, freqMs }),
|
||||
Intensity.Clamp4(new[] { intA, intA, intA, intA })));
|
||||
framesB.Add(new WaveFrame(
|
||||
Freq.Compress4(new[] { freqMs, freqMs, freqMs, freqMs }),
|
||||
Intensity.Clamp4(new[] { intB, intB, intB, intB })));
|
||||
}
|
||||
|
||||
_state.Stop('A'); _state.Stop('B');
|
||||
_state.EnqueueStream('A', framesA);
|
||||
_state.EnqueueStream('B', framesB);
|
||||
|
||||
// Wait for the stream to be consumed (~3s) plus margin, then silence.
|
||||
await Task.Delay(3300);
|
||||
|
||||
_state.SetStrength('A', 0);
|
||||
_state.SetStrength('B', 0);
|
||||
_state.Stop('A');
|
||||
_state.Stop('B');
|
||||
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
IsTestRunning = false;
|
||||
RefreshButtonStates();
|
||||
});
|
||||
}
|
||||
|
||||
void OnStop(object? sender, EventArgs e)
|
||||
{
|
||||
StopLive();
|
||||
StopPattern();
|
||||
StopRandom();
|
||||
_state.SetStrength('A', 0);
|
||||
_state.SetStrength('B', 0);
|
||||
_state.Stop('A');
|
||||
@@ -564,6 +571,16 @@ public class MainForm : Form
|
||||
_lblLimitValB.Text = $"{maxB}";
|
||||
}
|
||||
|
||||
void OnAmpChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var ampA = 1.0 + _ampBarA.Value / 100.0 * 9.0;
|
||||
var ampB = 1.0 + _ampBarB.Value / 100.0 * 9.0;
|
||||
_state.SetAmpA(ampA);
|
||||
_state.SetAmpB(ampB);
|
||||
_lblAmpValA.Text = $"{_ampBarA.Value}";
|
||||
_lblAmpValB.Text = $"{_ampBarB.Value}";
|
||||
}
|
||||
|
||||
void OnSwapChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_state.SwapChannels = _chkSwap.Checked;
|
||||
@@ -703,6 +720,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
|
||||
|
||||
@@ -79,6 +79,33 @@ public static class MusicAnalyzer
|
||||
public readonly List<double> MelodyFreqSamples = new();
|
||||
}
|
||||
|
||||
public static WaveFrame BuildMelodyFrame(TickFeature tf, double maxMelodyEnergy)
|
||||
{
|
||||
double avgEnergy = tf.MelodyCount > 0 ? tf.MelodyEnergy / tf.MelodyCount : 0;
|
||||
double normalizedEnergy = maxMelodyEnergy > 0 ? avgEnergy / maxMelodyEnergy : 0;
|
||||
int melodyIntensity = (int)Math.Round(normalizedEnergy * 80);
|
||||
melodyIntensity = Math.Clamp(melodyIntensity, 0, 100);
|
||||
|
||||
double weightedFreq = 0;
|
||||
double totalWeight = 0;
|
||||
foreach (var f in tf.MelodyFreqSamples)
|
||||
{
|
||||
weightedFreq += f * f;
|
||||
totalWeight += f;
|
||||
}
|
||||
double avgMelodyHz = totalWeight > 0 ? weightedFreq / totalWeight : 500;
|
||||
int estimsMs = MapPitchToPeriod(avgMelodyHz);
|
||||
|
||||
var intB = new[]
|
||||
{
|
||||
(byte)melodyIntensity, (byte)melodyIntensity,
|
||||
(byte)melodyIntensity, (byte)melodyIntensity
|
||||
};
|
||||
var freqB = Freq.Compress4(new[] { estimsMs, estimsMs, estimsMs, estimsMs });
|
||||
|
||||
return new WaveFrame(freqB, intB);
|
||||
}
|
||||
|
||||
public static (WaveFrame chA, WaveFrame chB) BuildWaveFrame(TickFeature tf, double maxFlux, double maxMelodyEnergy)
|
||||
{
|
||||
const int rhythmFreqMs = 150;
|
||||
|
||||
@@ -17,7 +17,7 @@ public class State
|
||||
|
||||
public int ActualA, ActualB;
|
||||
public int LastSentA, LastSentB;
|
||||
public int LastIntensityA, LastIntensityB;
|
||||
public double LimitScaleA, LimitScaleB;
|
||||
public byte[]? LastFreqA, LastIntA, LastFreqB, LastIntB;
|
||||
public bool LastActiveA, LastActiveB;
|
||||
|
||||
@@ -26,6 +26,8 @@ public class State
|
||||
public LimitMode LimitModeA = LimitMode.Clamp;
|
||||
public LimitMode LimitModeB = LimitMode.Clamp;
|
||||
public bool SwapChannels;
|
||||
public double AmpA = 1.0;
|
||||
public double AmpB = 1.0;
|
||||
|
||||
public void SetLimitA(int max, LimitMode mode)
|
||||
{
|
||||
@@ -47,6 +49,24 @@ public class State
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAmpA(double amp)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
AmpA = Math.Clamp(amp, 0.1, 10.0);
|
||||
DirtyA = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAmpB(double amp)
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
AmpB = Math.Clamp(amp, 0.1, 10.0);
|
||||
DirtyB = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSignaling
|
||||
{
|
||||
get
|
||||
@@ -115,20 +135,31 @@ public class State
|
||||
var valB = ApplyLimit(DesiredB, MaxB, LimitModeB);
|
||||
LastSentA = valA;
|
||||
LastSentB = valB;
|
||||
LimitScaleA = DesiredA > 0 ? (double)valA / DesiredA : 0;
|
||||
LimitScaleB = DesiredB > 0 ? (double)valB / DesiredB : 0;
|
||||
DirtyA = false;
|
||||
DirtyB = false;
|
||||
|
||||
bool hasA = !StreamA.IsEmpty || (LoopFreqA != null && LoopIntA != null);
|
||||
bool hasB = !StreamB.IsEmpty || (LoopFreqB != null && LoopIntB != null);
|
||||
var (freqA, intA) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
||||
var (freqB, intB) = PopWave(StreamB, LoopFreqB, LoopIntB);
|
||||
var (freqA, intARaw) = PopWave(StreamA, LoopFreqA, LoopIntA);
|
||||
var (freqB, intBRaw) = PopWave(StreamB, LoopFreqB, LoopIntB);
|
||||
|
||||
// Apply amplifier to intensity, clamp to 0-100
|
||||
var ampA = AmpA;
|
||||
var ampB = AmpB;
|
||||
var intA = new byte[4];
|
||||
var intB = new byte[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
intA[i] = (byte)Math.Clamp((int)Math.Round(intARaw[i] * ampA), 0, 100);
|
||||
intB[i] = (byte)Math.Clamp((int)Math.Round(intBRaw[i] * ampB), 0, 100);
|
||||
}
|
||||
|
||||
LastActiveA = hasA;
|
||||
LastActiveB = hasB;
|
||||
LastFreqA = freqA; LastIntA = intA;
|
||||
LastFreqB = freqB; LastIntB = intB;
|
||||
LastIntensityA = hasA ? (intA[0] + intA[1] + intA[2] + intA[3]) / 4 : 0;
|
||||
LastIntensityB = hasB ? (intB[0] + intB[1] + intB[2] + intB[3]) / 4 : 0;
|
||||
|
||||
if (SwapChannels)
|
||||
{
|
||||
@@ -140,7 +171,7 @@ public class State
|
||||
(LastFreqA, LastFreqB) = (LastFreqB, LastFreqA);
|
||||
(LastIntA, LastIntB) = (LastIntB, LastIntA);
|
||||
(LastActiveA, LastActiveB) = (LastActiveB, LastActiveA);
|
||||
(LastIntensityA, LastIntensityB) = (LastIntensityB, LastIntensityA);
|
||||
(LimitScaleA, LimitScaleB) = (LimitScaleB, LimitScaleA);
|
||||
}
|
||||
|
||||
return (modeA, valA, modeB, valB, freqA, intA, freqB, intB);
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AssemblyName>Substation</AssemblyName>
|
||||
<RootNamespace>Substation</RootNamespace>
|
||||
<Version>0.2.0</Version>
|
||||
<AssemblyVersion>0.2.0</AssemblyVersion>
|
||||
<FileVersion>0.2.0</FileVersion>
|
||||
<Version>0.3.0</Version>
|
||||
<AssemblyVersion>0.3.0</AssemblyVersion>
|
||||
<FileVersion>0.3.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user