diff --git a/Helpers/GainDb.cs b/Helpers/GainDb.cs new file mode 100644 index 0000000..f0deb1a --- /dev/null +++ b/Helpers/GainDb.cs @@ -0,0 +1,44 @@ +using System; + +namespace ytLive.Helpers; + +/// The fader scale for the audio dock: the model stores LINEAR gain (0..1), the dock +/// shows and edits DECIBELS, because "−6 dB" means something to a person and "0.50" does not. +/// +/// Range: −∞ (silence) to +6 dB of headroom above unity. 0 dB is unity gain, which is what +/// the old 1.0 meant — so a persisted 0.8 comes back as ≈ −1.9 dB and nothing about the stored +/// data changes. +/// +/// Creator context (2026-10-30): nearly deaf on the right, so the fader number is read, not +/// heard. The scale therefore has to be the true one, not a cosmetic curve. +public static class GainDb +{ + /// Silence floor. Anything at or below this reads as −∞ and writes as 0 gain. + public const double FloorDb = -60; + + public const double MaxDb = 6; + + /// Linear gain → dB, clamped to the fader's range. Zero becomes . + public static double ToDb(double gain) + { + if (gain <= 0) return FloorDb; + var db = 20 * Math.Log10(gain); + return Math.Clamp(db, FloorDb, MaxDb); + } + + /// dB → linear gain. At the floor the result is exactly 0 (true silence), so the + /// bottom of the fader cannot leave a residue of noise. + public static double FromDb(double db) + { + if (db <= FloorDb) return 0; + return Math.Pow(10, Math.Clamp(db, FloorDb, MaxDb) / 20); + } + + /// "−∞", "−6.0", "+1.5" — the label beside a fader. + public static string Label(double db) => + db <= FloorDb ? "−∞" : (db > 0 ? "+" : string.Empty) + db.ToString("0.0"); + + /// Mono fold: the accessible downmix. Averages the pair so hard-panned content is + /// audible on both sides at half amplitude instead of absent from one. + public static float MonoFold(float left, float right) => (left + right) * 0.5f; +} diff --git a/ytLive.Tests/GainDbTests.cs b/ytLive.Tests/GainDbTests.cs new file mode 100644 index 0000000..94e0134 --- /dev/null +++ b/ytLive.Tests/GainDbTests.cs @@ -0,0 +1,43 @@ +using Xunit; +using ytLive.Helpers; + +namespace ytLive.Tests; + +/// The dock's faders speak dB while the model stores linear gain, so the mapping is load +/// bearing: get it wrong and every reported level is wrong by a constant. Unity, the floor and +/// the +6 dB ceiling are the three values a creator actually checks. +public class GainDbTests +{ + [Fact] + public void Db_and_linear_gain_round_trip_and_the_ends_are_honest() + { + // Unity is the anchor: 1.0 ↔ 0 dB, and the default mic volume reads −1.9 dB. + Assert.Equal(0, GainDb.ToDb(1.0), 6); + Assert.Equal(1.0, GainDb.FromDb(0), 6); + Assert.Equal(-1.9, GainDb.ToDb(0.8), 1); + + // Halving is −6 dB — the number a creator reads as "half". + Assert.Equal(-6.02, GainDb.ToDb(0.5), 1); + + // The floor is TRUE silence: no residue left at the bottom of the fader. + Assert.Equal(0.0, GainDb.FromDb(GainDb.FloorDb)); + Assert.Equal(0.0, GainDb.FromDb(-999)); + + // Ceiling: +6 dB is twice the amplitude and nothing beyond it. + Assert.Equal(2.0, GainDb.FromDb(6), 2); + Assert.Equal(6, GainDb.ToDb(GainDb.FromDb(99)), 6); + + // Round-trip across the usable range. + foreach (var db in new[] { -60.0, -40, -20, -12, -6, -3, 0, 3, 6 }) + Assert.Equal(db, GainDb.ToDb(GainDb.FromDb(db)), 6); + + // Labels are what the creator reads. + Assert.Equal("−∞", GainDb.Label(GainDb.FloorDb)); + Assert.Equal("-6.0", GainDb.Label(-6)); + Assert.Equal("+1.5", GainDb.Label(1.5)); + + // The mono fold: a hard-right signal becomes audible on the left at half amplitude. + Assert.Equal(0.25f, GainDb.MonoFold(0f, 0.5f)); + Assert.Equal(0.5f, GainDb.MonoFold(0.5f, 0.5f)); + } +}