# Peak Meter Debug Summary (February 7, 2026) ## Side-Task Completed: Peak Visualization ✅ Added inline peak meter effect to test_demo for visual debugging of audio-visual synchronization. ### Implementation **Files Modified:** - `src/test_demo.cc`: Added `PeakMeterEffect` class inline (89 lines of WGSL + C++) - `src/gpu/gpu.h`: Added `gpu_add_custom_effect()` API and exposed `g_device`, `g_queue`, `g_format` - `src/gpu/gpu.cc`: Implemented `gpu_add_custom_effect()` to add effects to MainSequence at runtime **Peak Meter Features:** - Red horizontal bar in middle of screen (5% height) - Bar width extends from left (0.0) to peak_value (0.0-1.0) - Renders as final post-process pass (priority=999) - Only compiled in debug builds (`!STRIP_ALL`) **Visual Effect:** ``` Screen Layout: ┌─────────────────────────────────────┐ │ │ │ │ │ ████████████░░░░░░░░░░░░░░░░░ │ ← Red bar (width = audio peak) │ │ │ │ └─────────────────────────────────────┘ ``` ### WGSL Shader Code ```wgsl @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { let color = textureSample(inputTexture, inputSampler, input.uv); // Draw red horizontal bar in middle of screen let bar_height = 0.05; let bar_center_y = 0.5; let bar_y_min = bar_center_y - bar_height * 0.5; let bar_y_max = bar_center_y + bar_height * 0.5; let bar_x_max = uniforms.peak_value; let in_bar_y = input.uv.y >= bar_y_min && input.uv.y <= bar_y_max; let in_bar_x = input.uv.x <= bar_x_max; if (in_bar_y && in_bar_x) { return vec4(1.0, 0.0, 0.0, 1.0); // Red bar } else { return color; // Original scene } } ``` --- ## Main Issue: Audio Peak Timing Analysis 🔍 ### Problem Discovery The raw_peak values logged at beat boundaries don't match the expected drum pattern: **Expected Pattern** (from test_demo.track): ``` Beat 0, 2: Kick (volume 1.0) → expect raw_peak ~0.125 (after 8x = 1.0 visual) Beat 1, 3: Snare (volume 0.9) → expect raw_peak ~0.090 (after 8x = 0.72 visual) ``` **Actual Logged Peaks** (from peaks.txt): ``` Beat | Time | Raw Peak | Expected -----|-------|----------|---------- 0 | 0.19s | 0.588 | ~0.125 (kick) 1 | 0.50s | 0.177 | ~0.090 (snare) 2 | 1.00s | 0.236 | ~0.125 (kick) ← Too low! 3 | 1.50s | 0.199 | ~0.090 (snare) 4 | 2.00s | 0.234 | ~0.125 (kick) ← Too low! 5 | 2.50s | 0.475 | ~0.090 (snare) 9 | 4.50s | 0.975 | ~0.090 (snare) ← Should be kick! ``` ### Root Cause: Ring Buffer Latency **Ring Buffer Configuration:** - `RING_BUFFER_LOOKAHEAD_MS = 400` (src/audio/ring_buffer.h:14) - Audio is rendered 400ms ahead of playback - Real-time peak is measured when audio is actually played (in audio callback) - Visual timing uses `current_time` (physical time) **Timing Mismatch:** ``` Visual Beat 2 (T=1.00s) → Audio being played (T=1.00s - 0.40s = T=0.60s) → At T=0.60s, beat = 0.60 * 2 = 1.2 → Beat 1 (snare) → Visual expects kick, but hearing snare! ``` ### Peak Decay Analysis **Decay Configuration** (src/audio/backend/miniaudio_backend.cc:166): ```cpp realtime_peak_ *= 0.7f; // Decay: 30% per callback ``` **Decay Timing:** - Callback interval: ~128ms (at 4096 frames @ 32kHz) - To decay from 1.0 to 0.1: `0.7^n = 0.1` → n ≈ 6.45 callbacks - Time to 10%: 6.45 * 128ms = 825ms (~0.8 seconds) - Comment claims "~1 second decay" (line 162): `0.7^7.8 ≈ 0.1` **Problem:** - Drums hit every 0.5 seconds (120 BPM = 2 beats/second) - Decay takes 0.8-1.0 seconds - Peak doesn't drop fast enough between beats! **Calculation:** - After 0.5s (1 beat): `0.7^(0.5/0.128) = 0.7^3.9 ≈ 0.24` (raw peak) - Visual peak: `0.24 * 8 = 1.92` (clamped to 1.0) - Result: Visual peak stays at 1.0 between beats! --- ## Solutions ### Option A: Fix Ring Buffer Latency Alignment **Change:** Use audio playback time instead of current_time for visual effects. ```cpp // In test_demo.cc, replace current_time with audio-aligned time: const float audio_time = current_time - (RING_BUFFER_LOOKAHEAD_MS / 1000.0f); const float beat_time = audio_time * 120.0f / 60.0f; ``` **Pros:** Simple fix, aligns visual timing with heard audio **Cons:** Introduces 400ms visual lag (flash happens 400ms after visual beat) ### Option B: Compensate Peak Forward **Change:** Measure peak from future audio (at render time, not playback time). ```cpp // In synth.cc, measure peak when audio is rendered: float synth_get_output_peak() { return g_peak; // Peak measured at render time (400ms ahead) } ``` **Pros:** Zero visual lag, flash syncs with visual beat timing **Cons:** Flash happens 400ms BEFORE audio is heard (original bug!) ### Option C: Reduce Ring Buffer Latency **Change:** Decrease `RING_BUFFER_LOOKAHEAD_MS` from 400ms to 100ms. **Pros:** Smaller timing mismatch (100ms instead of 400ms) **Cons:** May cause audio underruns at 2.0x tempo scaling ### Option D: Faster Peak Decay **Change:** Increase decay rate to match beat interval. **Target:** Peak should drop below 0.7 (flash threshold) after 0.5s. **Calculation:** - Visual threshold: 0.7 - After 8x multiplier: raw_peak < 0.7/8 = 0.0875 - After 0.5s (3.9 callbacks): `decay_rate^3.9 < 0.0875` - `decay_rate < 0.0875^(1/3.9) = 0.493` **Recommended Decay:** 0.5 per callback (instead of 0.7) ```cpp // In miniaudio_backend.cc:166 realtime_peak_ *= 0.5f; // Decay: 50% per callback (~500ms to 10%) ``` **Pros:** Flash triggers only on actual hits, fast fade **Cons:** Very aggressive decay, might miss short drum hits --- ## Recommended Solution: Option A + Option D **Combined Approach:** 1. **Align visual beat timing** with audio playback (subtract 400ms) 2. **Faster decay** (0.5 instead of 0.7) to prevent overlapping flashes **Implementation:** ```cpp // test_demo.cc:209 (replace current_time calculation) const float audio_aligned_time = (float)current_time - 0.4f; // Subtract ring buffer latency const float beat_time = fmaxf(0.0f, audio_aligned_time) * 120.0f / 60.0f; // miniaudio_backend.cc:166 (update decay rate) realtime_peak_ *= 0.5f; // Decay: 50% per callback (faster) ``` **Expected Result:** - Visual flash triggers exactly when kick is HEARD (not 400ms early) - Flash decays quickly (~500ms) so snare doesn't re-trigger - Peak meter visualization shows accurate real-time audio levels --- ## Testing Checklist With peak meter visualization, verify: - [ ] Red bar extends when kicks hit (every 1 second at beats 0, 2, 4, ...) - [ ] Bar width matches FlashEffect intensity (both use same peak value) - [ ] Bar decays smoothly between hits - [ ] Snares (beats 1, 3, 5, ...) show smaller bar width (~60-70%) - [ ] With faster decay (0.5), bar reaches minimum before next hit --- ## Next Steps 1. **Implement Option A + D** (timing alignment + faster decay) 2. **Test with peak meter** to visually verify timing 3. **Log peaks with --log-peaks** to quantify improvement 4. **Consider Option C** (reduce ring buffer) if tempo scaling still works 5. **Update documentation** with final timing strategy --- *Created: February 7, 2026* *Peak meter visualization added, timing analysis complete*