TASK 8 meter scaling: +10 dB input amplification in AudioLevelMeter.ToDisplay so the meters use the full bar — speech peaks (~0.2 RMS, -14 dBFS) read ~0.93 (red zone) and normal speech (~0.05, -26 dBFS) ~0.73 (yellow) at maxed volume, instead of hovering at the red edge; ≤0.001 linear still reads zero so the meter never idles on background noise; one knob shared by the mic bar and the game bar (× MicVolume gain-on-noise untouched); tests updated + new ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume — 171 tests passing, 0 warnings
This commit is contained in:
@@ -8,6 +8,14 @@
|
||||
## Session state (last updated: 2026-08-13)
|
||||
|
||||
- **Branch:** `main`, in sync with `origin/main`.
|
||||
- **TASK 8 (new, after TASK 7 shipped):** `AudioLevelMeter.ToDisplay` gained **+10 dB input
|
||||
amplification** so the meters use the full bar — speech peaks (~0.2 RMS) now read ~0.93 (red)
|
||||
and normal speech (~0.05) ~0.73 (yellow) at maxed volume; ≤0.001 linear still reads 0 (idle
|
||||
never shows noise). One knob shared by the mic bar and game bar; `× MicVolume` untouched.
|
||||
Tests updated + new `ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume`. **Uncommitted**
|
||||
(pending user review): `Services/Audio/AudioLevelMeter.cs`, `ytLive.Tests/AudioMixerTests.cs`,
|
||||
`ai.md`, `Services/index.md`, `ViewModels/index.md`, `TASKS.md`, `HANDOFF.md`. Meter tests:
|
||||
7/7 passing, build 0 warnings.
|
||||
- **This session (TASK 7 — UI polish batch, gramps's 6-point review):**
|
||||
1. **Scenes list cleaned:** the per-row edit/trash/visibility icons and the inline
|
||||
rename TextBox are gone. Scenes are pure selection rows; `IsHidden` stays
|
||||
|
||||
@@ -15,15 +15,18 @@ public sealed class AudioLevelMeter
|
||||
|
||||
/// <summary>
|
||||
/// Maps a linear level (0..1) onto the meter's display scale: -60 dBFS..0 dBFS
|
||||
/// spread linearly across 0..1. Linear RMS of real speech or game audio is
|
||||
/// ~0.01..0.1 (-40..-20 dBFS), which leaves a flat (linear) meter looking
|
||||
/// dead; the log scale makes typical levels occupy the bar.
|
||||
/// spread linearly across 0..1, with +10 dB of input amplification. Linear RMS
|
||||
/// of real speech or game audio is ~0.01..0.1 (-40..-20 dBFS), which leaves a
|
||||
/// flat (linear) meter looking dead; the log scale makes typical levels occupy
|
||||
/// the bar and the amplification pushes real speech peaks into the red zone
|
||||
/// (0.8+) at maxed volume instead of hovering at its edge. Inputs at or below
|
||||
/// -60 dBFS (0.001 linear) read as zero — the meter never idles on background noise.
|
||||
/// </summary>
|
||||
public static float ToDisplay(float linear)
|
||||
{
|
||||
if (linear <= 0.001f)
|
||||
return 0f;
|
||||
var db = 20f * MathF.Log10(linear);
|
||||
var db = 20f * MathF.Log10(linear) + 10f;
|
||||
return Math.Clamp(1f + db / 60f, 0f, 1f);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ External-facing logic: YouTube API, persistence. See
|
||||
| `Audio/WasapiLoopbackAudioSource.cs` | Desktop/game capture: NAudio `WasapiLoopbackCapture` on the default render device — automatic at unity; the game bar's meter consumes it |
|
||||
| `Audio/WasapiMicAudioSource.cs` | Mic capture: NAudio `WasapiCapture` with the device resolved by `FriendlyName` matching `MicSourceName` (DisplayName only), default capture endpoint fallback; name provider `Func<string?>` re-read at each `Start` so a device picked mid-session takes effect immediately (the mixer restarts the mic on pick) |
|
||||
| `Audio/AudioMixer.cs` | Owns both sources; capture starts once at startup (`MainViewModel.StartMicCaptureAsync`) and stops on `Shutdown` — NOT go-live (preview monitoring). Mic samples → `AudioLevelMeter` → `MicLevelChanged`; loopback samples → the game bar's meter via `LoopbackLevelChanged`. Surfaces mic connection state: `MicConnected`/`MicFailed` (drives the status dot) + `RestartMic()` (device swap mid-session, keeps loopback). Failures log via `AppLog`; mic failure zeroes the meter, loopback failure doesn't kill the mic. Note: the meter `Push` is unconditional (the `?.` on the event would otherwise skip the argument when nothing is subscribed) |
|
||||
| `Audio/AudioLevelMeter.cs` | Pure smoothed RMS level (0..1): `Push(AudioSample)` + `Reset` — the unit-tested math behind `AudioLevel` and `GameAudioLevel`. `ToDisplay(float)` maps the raw linear RMS onto the meter's display scale (−60..0 dBFS spread across 0..1) — real speech/game RMS (~0.01..0.1) would otherwise leave a flat scale looking dead |
|
||||
| `Audio/AudioLevelMeter.cs` | Pure smoothed RMS level (0..1): `Push(AudioSample)` + `Reset` — the unit-tested math behind `AudioLevel` and `GameAudioLevel`. `ToDisplay(float)` maps the raw linear RMS onto the meter's display scale (−60..0 dBFS spread across 0..1, with **+10 dB input amplification** so real speech peaks hit the red zone at maxed volume) — real speech/game RMS (~0.01..0.1) would otherwise leave a flat scale looking dead; ≤0.001 linear reads as zero (never idles on background noise) |
|
||||
| `Audio/WaveToFloat.cs` | Pure WASAPI buffer → float conversion: IEEE float 32-bit direct, PCM 16-bit normalized, `WaveFormatExtensible` IEEE-float subformat GUID, trailing partial samples ignored |
|
||||
| `IGameAudioDetector.cs` | **TASK 4 game audio bar seam**: `IsGameAudioActive` + `IsGameAudioActiveChanged` + `Poll()` — detects "a working game with sound is up in the preview" |
|
||||
| `GameAudioHysteresis.cs` | Pure show/hide state machine for the game bar: SHOW = full-screen app holds sound ~500ms; HIDE = the app leaves fullscreen ~1s. **Silence never hides an active bar** — only the game leaving the preview does (creator's rule). No timers; `Update(isFullScreen, hasSound, now)` |
|
||||
|
||||
@@ -623,6 +623,22 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
|
||||
|
||||
---
|
||||
|
||||
## TASK 8 — Meter scaling amplification (voice meter uses the full bar)
|
||||
|
||||
**Goal:** the mic (and game) meters feel light — speech peaks should peg into the red at maxed volume.
|
||||
|
||||
### Status: ✅ Done
|
||||
|
||||
1. ✅ `AudioLevelMeter.ToDisplay` now adds **+10 dB of input amplification** before the −60..0 dBFS → 0..1 log mapping (was raw dB): speech peaks (~0.2 RMS, −14 dBFS) read ~0.93 → red zone; normal speech (~0.05, −26 dBFS) ~0.73 → yellow; background noise ≤0.001 linear (−60 dBFS) still reads 0 (the meter never idles on it)
|
||||
2. ✅ Unit tests updated to the new mapping + new `ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume` (0.2 → 0.92..0.95, 0.05 → 0.72..0.75)
|
||||
|
||||
### Design decisions
|
||||
|
||||
1. **Amplify at the mapping, not in the VM** — one knob (`ToDisplay`), shared by the mic bar and the game bar; the `× MicVolume` gain-on-noise behavior is untouched (raising the slider still moves ambient noise up the bar).
|
||||
2. **+10 dB, not more** — louder boosts push quiet speech into the lower half and make the noise floor visible; +10 puts speech peaks solidly in red while idle stays at zero.
|
||||
|
||||
---
|
||||
|
||||
## Backlog (future versions)
|
||||
|
||||
1. v0.2 — Recording to local file (recordings carry the branding flash — see TASK 3 / `ai.md` Monetization)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -383,8 +383,11 @@ devices, no timers).
|
||||
no longer touch the mixer). Mic samples feed a pure **`AudioLevelMeter`** (RMS with 0.2 exponential
|
||||
smoothing) → `MicLevelChanged` → marshalled to the UI thread → `AudioLevel`; loopback samples feed a
|
||||
second meter → `LoopbackLevelChanged` → the game bar's `GameAudioLevel`. The raw linear level is
|
||||
mapped to the meter's display scale by `AudioLevelMeter.ToDisplay` (−60..0 dBFS → 0..1): real speech/
|
||||
game RMS is ~0.01..0.1 linear, which would leave a flat scale looking dead. **Mic connection state
|
||||
mapped to the meter's display scale by `AudioLevelMeter.ToDisplay` (−60..0 dBFS → 0..1 with **+10 dB
|
||||
input amplification**): real speech/game RMS is ~0.01..0.1 linear, which would leave a flat scale
|
||||
looking dead; the amplification (added 2026-08-14) pushes real speech peaks (~0.2 RMS, −14 dBFS) to
|
||||
~0.93 at maxed volume so the meter uses the whole bar proportionately instead of hovering at the red
|
||||
edge. **Mic connection state
|
||||
surfaces as events:** `MicConnected` (source `Started`), `MicFailed` (source `Failed`), and
|
||||
`RestartMic()` re-resolves + restarts just the mic (loopback keeps running). Failures log via
|
||||
`AppLog`; a mic failure zeroes the meter, a loopback failure never kills the mic. Meter `Push` is
|
||||
|
||||
@@ -331,11 +331,22 @@ 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 a decade (~-20 dB) is 1/3 up the bar.
|
||||
// -60 dBFS floor → 0, 0 dBFS → 1, and +10 dB amplification: a decade
|
||||
// (~-20 dB) is 2/3 up the bar.
|
||||
Assert.Equal(1f, AudioLevelMeter.ToDisplay(1f), 3);
|
||||
Assert.Equal(0.5f, AudioLevelMeter.ToDisplay(0.0316f), 2);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.1f), 0.66f, 0.67f);
|
||||
Assert.InRange(AudioLevelMeter.ToDisplay(0.01f), 0.32f, 0.34f);
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume()
|
||||
{
|
||||
// ~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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user