Files
Substation/State.cs
T
mute 33e289d596 Add per-channel independent limit controls
- Separate Lim A / Lim B trackbars (0-200, default 30 each)
- Separate Scale A / Scale B checkboxes (clamp vs scale per channel)
- State.SetLimitA/SetLimitB with independent LimitModeA/LimitModeB
- ConsumeTick applies per-channel limit and mode
2026-08-08 11:26:44 +00:00

149 lines
4.5 KiB
C#

using System.Collections.Concurrent;
namespace Substation;
public class State
{
public readonly object Lock = new();
public bool DirtyA, DirtyB;
public int DesiredA, DesiredB;
public byte[]? LoopFreqA, LoopIntA;
public byte[]? LoopFreqB, LoopIntB;
public readonly ConcurrentQueue<WaveFrame> StreamA = new();
public readonly ConcurrentQueue<WaveFrame> StreamB = new();
public int ActualA, ActualB;
public int LastSentA, LastSentB;
public int LastIntensityA, LastIntensityB;
public byte[]? LastFreqA, LastIntA, LastFreqB, LastIntB;
public bool LastActiveA, LastActiveB;
public int MaxA = 200;
public int MaxB = 200;
public LimitMode LimitModeA = LimitMode.Clamp;
public LimitMode LimitModeB = LimitMode.Clamp;
public void SetLimitA(int max, LimitMode mode)
{
lock (Lock)
{
MaxA = Math.Clamp(max, 0, 200);
LimitModeA = mode;
DirtyA = true;
}
}
public void SetLimitB(int max, LimitMode mode)
{
lock (Lock)
{
MaxB = Math.Clamp(max, 0, 200);
LimitModeB = mode;
DirtyB = true;
}
}
public bool IsSignaling
{
get
{
lock (Lock)
{
bool hasPattern = LoopFreqA != null || LoopFreqB != null
|| !StreamA.IsEmpty || !StreamB.IsEmpty;
bool hasStrength = DesiredA > 0 || DesiredB > 0;
return hasPattern && hasStrength;
}
}
}
public void SetStrength(char ch, int value)
{
lock (Lock)
{
var clamped = Math.Clamp(value, 0, 200);
if (ch == 'A') { DesiredA = clamped; DirtyA = true; }
else { DesiredB = clamped; DirtyB = true; }
}
}
public void SetLoop(char ch, byte[] freq, byte[] intensity)
{
lock (Lock)
{
if (ch == 'A') { LoopFreqA = freq; LoopIntA = intensity; }
else { LoopFreqB = freq; LoopIntB = intensity; }
}
}
public void EnqueueStream(char ch, IEnumerable<WaveFrame> frames)
{
var queue = ch == 'A' ? StreamA : StreamB;
foreach (var f in frames)
queue.Enqueue(f);
}
public void Stop(char ch)
{
lock (Lock)
{
if (ch == 'A') { LoopFreqA = null; LoopIntA = null; }
else { LoopFreqB = null; LoopIntB = null; }
}
var queue = ch == 'A' ? StreamA : StreamB;
while (queue.TryDequeue(out _)) { }
}
public (StrengthMode modeA, byte valA, StrengthMode modeB, byte valB,
byte[] freqA, byte[] intA, byte[] freqB, byte[] intB)
ConsumeTick()
{
lock (Lock)
{
var modeA = DirtyA ? StrengthMode.Abs : StrengthMode.None;
var modeB = DirtyB ? StrengthMode.Abs : StrengthMode.None;
byte ApplyLimit(int desired, int max, LimitMode mode) =>
mode == LimitMode.Scale
? (byte)(desired * max / 200)
: (byte)Math.Clamp(desired, 0, max);
var valA = ApplyLimit(DesiredA, MaxA, LimitModeA);
var valB = ApplyLimit(DesiredB, MaxB, LimitModeB);
LastSentA = valA;
LastSentB = valB;
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);
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;
return (modeA, valA, modeB, valB, freqA, intA, freqB, intB);
}
}
static (byte[] freq, byte[] intensity) PopWave(
ConcurrentQueue<WaveFrame> stream,
byte[]? loopFreq, byte[]? loopInt)
{
if (stream.TryDequeue(out var frame))
return (frame.Freq, frame.Intensity);
if (loopFreq != null && loopInt != null)
return (loopFreq, loopInt);
return (B0.ChannelOff, B0.IntensityOff);
}
}