namespace TravelEar.Core; /// /// The channel fade a peer hears when a voice-activation trigger opens and closes its channel /// (Dissonance VoiceBroadcastTrigger._activationFader driven by the trigger's /// VolumeFaderSettings fade-in and fade-out), applied to Local Voice per sample so the /// transmit gate opens and closes as a ramp, not a cut. M2 run 3 heard the hard gate as chop: the /// game's VAD flag flips several times per phrase and every flip past the hold spliced a zero /// frame into speech. In a solo session the game's own fader never runs (the channel never opens), /// so the mod applies the same fade itself. Linear, like the game's fader; a zero fade jumps. /// Pure; encoder thread only, apart from which the main thread may call. /// public sealed class TransmitFader { private double _fadeInMs; private double _fadeOutMs; private double _gain; // 0..1, accumulated in double so 4800 steps of 1/4800 land exactly on 1 public TransmitFader(double fadeInMs, double fadeOutMs) { Set(fadeInMs, fadeOutMs); } public double FadeInMs => _fadeInMs; public double FadeOutMs => _fadeOutMs; /// The gain the last sample was scaled by (0 = silent, 1 = through). public float Gain => (float)_gain; /// Changes the fade times. The current gain is kept, so a change mid-fade never jumps. public void Set(double fadeInMs, double fadeOutMs) { if (fadeInMs < 0 || double.IsNaN(fadeInMs)) throw new ArgumentOutOfRangeException(nameof(fadeInMs), fadeInMs, "The fade-in must be zero or positive."); if (fadeOutMs < 0 || double.IsNaN(fadeOutMs)) throw new ArgumentOutOfRangeException(nameof(fadeOutMs), fadeOutMs, "The fade-out must be zero or positive."); _fadeInMs = fadeInMs; _fadeOutMs = fadeOutMs; } // [impl->REQ-RENDER-CLEAN] /// /// Scales one mono frame in place, ramping the gain linearly toward 1 while /// and toward 0 otherwise, at the fade rate for that direction. Returns true when the whole frame /// came out silent (gain 0 throughout), so the caller may push zeros instead. /// public bool Apply(Span frame, bool open, int sampleRate) { if (sampleRate <= 0) throw new ArgumentOutOfRangeException(nameof(sampleRate), sampleRate, "The sample rate must be positive."); var target = open ? 1.0 : 0.0; var gain = _gain; if (gain == target) { if (target == 0.0) frame.Clear(); return target == 0.0; } var fadeMs = open ? _fadeInMs : _fadeOutMs; var step = fadeMs <= 0 ? 1.0 : 1000.0 / (fadeMs * sampleRate); var silent = true; for (var i = 0; i < frame.Length; i++) { if (gain < target) { gain += step; if (gain > target) gain = target; } else if (gain > target) { gain -= step; if (gain < target) gain = target; } if (gain == 0.0) { frame[i] = 0f; continue; } silent = false; frame[i] = (float)(frame[i] * gain); } _gain = gain; return silent; } }