TASK 25: master limiter on the live mix (2026-08-15, queued from the TRAX discussion) — the review confirmed two of the creator's three instincts were already satisfied (MediaFoundationReader streams from disk so file size needs no guard; the MusicPlayer 0.20 bed rides the same loopback gain as the game, so music is always exactly 20% of the desktop volume by construction) and found one real gap: AudioMixer.FillAndMix summed mic + loopback with no output ceiling, so hot gains could pass 0 dBFS and clip the AAC encode. New pure Services/Audio/MasterLimiter.cs: -1 dBFS ceiling (Ceiling = 0.891), instant attack per frame (a hot frame is scaled exactly to the ceiling — no overshoot), smoothed release toward unity so loud passages don't pump, gain never exceeds 1. Applied at the end of FillAndMix right before the pipe write. 3 unit tests (trim-to-ceiling, never-boost, recovery) + ONE integration test (MasterLimiter_CapsTheLiveMix_OnThePipe: a 0.95 loopback bed capped to exactly 0.891 on the wire while staying audible). Docs in the same commit: ai.md go-live audio section, Services/index.md new row, TASKS.md TASK 25 record, HANDOFF.md shipped state. Build 0 warnings, 193 tests passing (189 + 4 new)
This commit is contained in:
@@ -11,8 +11,9 @@ namespace ytLive.Services.Audio;
|
||||
/// level-metered and queued; loopback samples are resampled to 48 kHz stereo
|
||||
/// and queued. While live (<see cref="StartLive"/>), a 10 ms loop drains both
|
||||
/// queues, applies the honest gains (mic = MicVolume, loopback = GameAudioVolume
|
||||
/// × auto-duck when the mic is hot), mixes them to stereo float and writes the
|
||||
/// chunk to the encoder's audio pipe.
|
||||
/// × auto-duck when the mic is hot), mixes them to stereo float, runs the
|
||||
/// master limiter (a -1 dBFS ceiling so the sum never clips the encoder), and
|
||||
/// writes the chunk to the encoder's audio pipe.
|
||||
/// </summary>
|
||||
public sealed class AudioMixer : IDisposable
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public sealed class AudioMixer : IDisposable
|
||||
private readonly AudioRingBuffer _loopbackBuffer;
|
||||
private readonly VoiceFilterChain _voiceChain;
|
||||
private readonly AutoDucker _ducker;
|
||||
private readonly MasterLimiter _masterLimiter = new();
|
||||
private TinyResampler? _micResampler;
|
||||
private TinyResampler? _loopbackResampler;
|
||||
private CancellationTokenSource? _liveCts;
|
||||
@@ -330,6 +332,7 @@ public sealed class AudioMixer : IDisposable
|
||||
mix[i * 2] = m + loopbackChunk[i * 2] * loopGain;
|
||||
mix[i * 2 + 1] = m + loopbackChunk[i * 2 + 1] * loopGain;
|
||||
}
|
||||
_masterLimiter.Process(mix);
|
||||
return micRms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace ytLive.Services.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Master output limiter — the last stage before the live mix reaches the
|
||||
/// encoder pipe: a peak ceiling of -1 dBFS so mic + loopback (game + TRAX music)
|
||||
/// can never sum past 0 dBFS and clip in the AAC encode. Instant attack per frame
|
||||
/// (a hot frame is scaled exactly to the ceiling — no overshoot), smoothed release
|
||||
/// toward unity so loud passages don't pump. Pure — unit-tested.
|
||||
/// </summary>
|
||||
public sealed class MasterLimiter
|
||||
{
|
||||
/// <summary>-1 dBFS: standard streaming headroom for encoder overshoot.</summary>
|
||||
public const float Ceiling = 0.891f;
|
||||
|
||||
private const float Release = 0.005f;
|
||||
private float _gain = 1f;
|
||||
|
||||
/// <summary>The current gain multiplier (≤ 1, so the limiter never boosts).</summary>
|
||||
public float Gain => _gain;
|
||||
|
||||
/// <summary>Scales the frame by the current gain. Gain drops instantly to
|
||||
/// <c>ceiling/peak</c> when the frame exceeds the ceiling and recovers
|
||||
/// toward unity slowly.</summary>
|
||||
public void Process(float[] samples)
|
||||
{
|
||||
var peak = 0f;
|
||||
foreach (var s in samples)
|
||||
{
|
||||
var abs = MathF.Abs(s);
|
||||
if (abs > peak)
|
||||
peak = abs;
|
||||
}
|
||||
|
||||
var target = peak > Ceiling ? Ceiling / peak : 1f;
|
||||
if (target < _gain)
|
||||
_gain = target;
|
||||
else
|
||||
_gain += (target - _gain) * Release;
|
||||
|
||||
for (var i = 0; i < samples.Length; i++)
|
||||
samples[i] *= _gain;
|
||||
}
|
||||
|
||||
public void Reset() => _gain = 1f;
|
||||
}
|
||||
Reference in New Issue
Block a user