summaryrefslogtreecommitdiff
path: root/doc/PEAK_METER_DEBUG.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/PEAK_METER_DEBUG.md')
-rw-r--r--doc/PEAK_METER_DEBUG.md224
1 files changed, 224 insertions, 0 deletions
diff --git a/doc/PEAK_METER_DEBUG.md b/doc/PEAK_METER_DEBUG.md
new file mode 100644
index 0000000..002180c
--- /dev/null
+++ b/doc/PEAK_METER_DEBUG.md
@@ -0,0 +1,224 @@
+# 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<f32> {
+ 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<f32>(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*