Audio: speech leveler (auto gain to -20 dBFS speech, -6..+20 dB, gated) between gate and compressor; honest mic meter (drop the +10 dB display boost and x1.2). Creator upload measured -28.5 LUFS; same audio through the new chain: -17.9 LUFS / -0.7 dBFS peak (Unit: audio loudness)
This commit is contained in:
@@ -331,22 +331,20 @@ public class AudioLevelMeterTests
|
||||
Assert.Equal(0f, AudioLevelMeter.ToDisplay(0f));
|
||||
Assert.Equal(0f, AudioLevelMeter.ToDisplay(0.001f), 2);
|
||||
|
||||
// -60 dBFS floor → 0, 0 dBFS → 1, and +10 dB amplification: a decade
|
||||
// (~-20 dB) is 2/3 up the bar.
|
||||
// -60 dBFS floor → 0, 0 dBFS → 1, honest in between (no hidden boost).
|
||||
Assert.Equal(1f, AudioLevelMeter.ToDisplay(1f), 3);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.0316f), 0.66f, 0.67f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.1f), 0.83f, 0.84f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.01f), 0.49f, 0.51f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.0316f), 0.49f, 0.51f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.1f), 0.66f, 0.67f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.01f), 0.33f, 0.34f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume()
|
||||
public void ToDisplay_Puts_Leveled_Speech_At_The_Green_Yellow_Edge_And_Peaks_In_Red()
|
||||
{
|
||||
// ~0.2 linear RMS (-14 dBFS) is a loud speech peak; at maxed volume it
|
||||
// must land in the red zone (0.8+) and normal speech (~0.05, -26 dBFS)
|
||||
// in the yellow — the meter uses the whole bar proportionately.
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.2f), 0.92f, 0.95f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.05f), 0.72f, 0.75f);
|
||||
// The speech leveler targets -20 dBFS: that sits at 2/3 of the bar (the yellow
|
||||
// zone starts at 60%); a hot -6 dBFS peak reaches the red (80%+).
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(SpeechLeveler.TargetRms), 0.66f, 0.67f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.5f), 0.89f, 0.91f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -98,6 +98,51 @@ public class AudioPipelineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>2026-10-09 — a creator's upload measured −28.5 LUFS (YouTube's reference is
|
||||
/// −14): nothing in the chain brought quiet speech up. THE test for the speech leveler:
|
||||
/// syllable-shaped "speech" fed through the REAL voice chain lands at the leveler's
|
||||
/// −20 dBFS target (±3 dB) whether the mic is quiet (−38 dBFS) or hot (−12 dBFS), and the
|
||||
/// pauses between phrases stay silent (the gain is never pumped into the noise).</summary>
|
||||
[Theory]
|
||||
[InlineData(-38.0)]
|
||||
[InlineData(-12.0)]
|
||||
public void VoiceChain_LevelsQuietAndHotSpeech_ToTheTarget_AndKeepsPausesSilent(double inputDbfs)
|
||||
{
|
||||
const int rate = 48000;
|
||||
var chain = new VoiceFilterChain(rate);
|
||||
|
||||
// Unit-RMS "voice": three partials, 4 Hz syllable envelope, then scaled to the input level.
|
||||
float Voice(int n)
|
||||
{
|
||||
var t = n / (float)rate;
|
||||
var tone = MathF.Sin(2 * MathF.PI * 210 * t) + 0.6f * MathF.Sin(2 * MathF.PI * 420 * t) + 0.3f * MathF.Sin(2 * MathF.PI * 840 * t);
|
||||
var syllable = MathF.Abs(MathF.Sin(2 * MathF.PI * 4 * t));
|
||||
return tone * syllable;
|
||||
}
|
||||
double unitSq = 0;
|
||||
for (var n = 0; n < rate; n++) unitSq += Voice(n) * (double)Voice(n);
|
||||
var scale = (float)(Math.Pow(10, inputDbfs / 20) / Math.Sqrt(unitSq / rate));
|
||||
|
||||
// 12 s: speak 2 s, pause 0.5 s, repeat. Measure the last 4 s.
|
||||
double speechSq = 0; long speechN = 0;
|
||||
var pauseMax = 0f;
|
||||
var total = 12 * rate;
|
||||
for (var n = 0; n < total; n++)
|
||||
{
|
||||
var phase = n % (int)(2.5 * rate);
|
||||
var speaking = phase < 2 * rate;
|
||||
var output = chain.Process(speaking ? Voice(n) * scale : 0f);
|
||||
if (n < total - 4 * rate) continue;
|
||||
if (speaking) { speechSq += output * (double)output; speechN++; }
|
||||
else if (phase > 2 * rate + rate / 10) pauseMax = MathF.Max(pauseMax, MathF.Abs(output)); // after the gate's release tail
|
||||
}
|
||||
|
||||
var speechDb = 10 * Math.Log10(speechSq / speechN);
|
||||
var targetDb = 20 * Math.Log10(SpeechLeveler.TargetRms);
|
||||
Assert.InRange(speechDb, targetDb - 3, targetDb + 3);
|
||||
Assert.InRange(pauseMax, 0f, 1e-4f); // < -80 dBFS: only the EQ's decaying tail, never boosted noise
|
||||
}
|
||||
|
||||
// ─── Ring buffer ───
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user