namespace TravelEar.Core;
/// What the gate wants done with one decoded Outbound Voice frame.
public enum GateDecision
{
/// Push the decoded frame: peers are receiving this audio.
Pass,
/// Push a frame of zeros of the same length instead: peers are not receiving it.
Silence,
}
///
/// The transmit gate (M2 T0, docs/DESIGN.md "Continuity"): decides, per decoded frame, whether the
/// Local Voice ring receives the frame or an equal length of silence. The encoder runs whenever the
/// mic is open (push-to-talk defaults to toggle-on and the game's "Self Echo" room channel is
/// always open), so Outbound Voice carries the mic noise floor between words; peers, however, only
/// receive frames while a voice-activation or push-to-talk channel is open. Gating on that signal
/// removes the noise floor without ever removing a frame: the ring stays fresh and continuous.
///
/// A short release hold keeps the gate open after the signal drops so the tail of a word is not
/// cut when the game's channel closes a frame or two early. There is no attack delay: the first
/// frame seen while transmitting passes.
///
///
/// Pure and single-threaded: the caller (the encoder thread) samples the transmit signal and the
/// clock and passes both in. Counters may be read from any thread for diagnostics.
///
///
public sealed class TransmitGate
{
/// Default release hold: about two 60 ms Opus frames.
public const double DefaultReleaseHoldMs = 100;
private double _releaseHoldMs;
private double _lastTransmitMs = double.NegativeInfinity;
private long _framesPassed;
private long _framesSilenced;
public TransmitGate(double releaseHoldMs = DefaultReleaseHoldMs)
{
SetReleaseHold(releaseHoldMs);
}
/// How long the gate stays open after the transmit signal drops, in milliseconds.
public double ReleaseHoldMs => _releaseHoldMs;
///
/// Changes the hold without touching the counters or the last-transmit time; the next
/// decision uses it. The renderer grows the hold to cover the channel fade-out once the
/// game's fade times are known ().
///
public void SetReleaseHold(double releaseHoldMs)
{
if (releaseHoldMs < 0 || double.IsNaN(releaseHoldMs))
throw new ArgumentOutOfRangeException(nameof(releaseHoldMs), releaseHoldMs, "The release hold must be zero or positive.");
_releaseHoldMs = releaseHoldMs;
}
/// Frames that passed through since construction.
public long FramesPassed => Volatile.Read(ref _framesPassed);
/// Frames replaced by silence since construction.
public long FramesSilenced => Volatile.Read(ref _framesSilenced);
// [impl->REQ-VOICE-CONTINUOUS]
///
/// Decides for one frame. is the "peers receive" signal as
/// sampled for this frame; is a monotonic clock in milliseconds. Every
/// call yields exactly one of or :
/// the gate never drops a frame, so the ring's write head advances by one frame per call
/// whichever way it decides (the never-gap invariant).
///
public GateDecision Decide(bool transmitting, double nowMs)
{
if (transmitting) _lastTransmitMs = nowMs;
var open = transmitting || nowMs - _lastTransmitMs <= _releaseHoldMs;
if (open) Interlocked.Increment(ref _framesPassed);
else Interlocked.Increment(ref _framesSilenced);
return open ? GateDecision.Pass : GateDecision.Silence;
}
}