fix(audio): game meter scales by GameAudioVolume — meter/stream/headphones all follow the one knob

Creator report 2026-09-01: with the desktop slider dragged to 20%, the game bar's
meter stayed pegged yellow/red. Root cause: the mic meter's formula (level x volume)
was cloned to the game bar WITHOUT the x-volume factor, and the clone inherited a
false premise — that WASAPI loopback capture tracks the endpoint volume. Tonight's
observation disproves it (the tap is pre-endpoint-volume), so both the meter (display)
and the mix (AudioGainProvider.LoopbackGain, fixed in 5ead064) must scale it themselves.
ai.md's two 'loopback scales with the slider' sentences corrected in the same pass.

Test seam: VolumePushOverride (internal static, mirrors LayoutPathOverride pattern) so
the test never hijacks the machine's real volume. GameMeterHonestyTests (RealApp +
temp DB): unity -> hot/red, 20% -> ~1/5 width/green, push observed.
This commit is contained in:
2026-09-01 21:22:57 -07:00
parent 5ead064d54
commit 7f2bda8ed3
3 changed files with 96 additions and 7 deletions
+72
View File
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Services;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// Game-bar meter honesty (creator bug 2026-09-01): the WASAPI loopback tap is
/// PRE-endpoint-volume, so lowering the desktop slider left the meter pegged
/// yellow/red at 20% — the mic formula scales level × volume, the game clone
/// dropped the factor. The meter, the stream gain, and the headphones must all
/// follow the one knob.
/// </summary>
[Collection("RealApp")]
public sealed class GameMeterHonestyTests
{
private readonly RealAppHost _app;
public GameMeterHonestyTests(RealAppHost app) => _app = app;
[Fact]
public void Game_Meter_Scales_By_Volume()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-gamemeter-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
var pushed = new List<double>();
MainViewModel.VolumePushOverride = v => pushed.Add(v);
var window = default(MainWindow);
try
{
using (var schema = new LayoutStore(tempDb)) { }
SqliteConnection.ClearAllPools();
window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
window.Show();
// Hot desktop bed pegged at full linear level (ToDisplay ~= 1.0).
vm.GameAudioVolume = 1.0;
vm.GameAudioLevel = 1.0;
Assert.True(vm.GameMeterFillWidth > 230, "unity volume must keep the meter hot");
Assert.Equal("#ef4444", vm.GameMeterBrush);
// Drop the knob to 20% — the captured level does NOT change (the OS
// tap is pre-endpoint-volume); the meter must scale it anyway.
vm.GameAudioVolume = 0.2;
vm.GameAudioLevel = 1.0;
Assert.InRange(vm.GameMeterFillWidth, 40, 80);
Assert.Equal("#22c55e", vm.GameMeterBrush);
// And the push really reached the (faked) endpoint — one knob, honest.
Assert.Contains(0.2, pushed);
}
finally
{
MainViewModel.VolumePushOverride = null;
if (window != null) window.Close();
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort */ }
}
}
}