# Audio Timing Architecture - Analysis and Proposed Solution (February 8, 2026) ## Problem Statement **Original Issue:** "demo is still flashing a lot" due to audio-visual timing mismatch. **Root Causes:** 1. Multiple time sources with no clear hierarchy. 2. Beat calculation for visuals uses the wrong time source (physical time instead of audio playback time). 3. Hardcoded peak decay rate does not adapt to the music's tempo. 4. Hardcoded BPM values in some places prevent data-driven tempo changes. --- ## Current State Analysis (The Problem) The current implementation suffers from several discrepancies that lead to audio-visual desynchronization. ### Discrepancy #1: Wrong Time Source for Beat Calculation Both `main.cc` and `test_demo.cc` use the physical wall clock time to calculate the current musical beat for visual effects. However, the audio peak is measured at the moment the audio is actually played, which can be hundreds of milliseconds later due to audio buffering. **`main.cc` (Incorrect Logic):** ```cpp // current_time is derived from platform_state.time (physical clock) const double current_time = platform_state.time + seek_time; // Beat is calculated from physical time const float beat_time = (float)current_time * g_tracker_score.bpm / 60.0f; // Peak is from audio playback time (a different clock) const float raw_peak = audio_get_realtime_peak(); // MISMATCH: `beat_time` and `raw_peak` are out of sync! ``` This is the primary cause of the AV sync issue. ### Discrepancy #2: Hardcoded Configuration The system relies on several hardcoded values instead of being data-driven. - **Hardcoded BPM:** `src/test_demo.cc` hardcodes the BPM to `120.0f`, ignoring the value in the track file. - **Hardcoded Peak Decay:** `src/audio/backend/miniaudio_backend.cc` uses a fixed decay rate of `0.5f`, which does not adapt to different tempos. A fast song will have its visual peak decay too slowly, and a slow song too quickly. - **No `tracker_get_bpm()` API:** There is no formal function to get the BPM from the tracker; code accesses the global `g_tracker_score.bpm` directly. --- ## Proposed Architecture (The Solution) ✅ ### Single Source of Truth: Physical Clock The core principle remains: `platform_get_time()` is the one authoritative wall clock from the OS. All other time representations should derive from it in a managed way. ``` Physical Time (platform_get_time()) ↓ ┌────────────────────────────────────────────────┐ │ Audio System tracks its own state: │ │ • audio_get_playback_time() │ │ → Based on ring buffer samples consumed │ │ → Automatically accounts for buffering │ │ → NO hardcoded constants! │ └────────────────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────┐ │ Music Time (tracker time) │ │ • Derived from audio playback time │ │ • Scaled by tempo_scale │ │ • Used by tracker for event triggering │ └────────────────────────────────────────────────┘ ``` ### Time Sources Summary | Time Source | Purpose | How to Get | Use For | |-------------|---------|------------|---------| | **Physical Time** | Wall clock, frame deltas | `platform_get_time()` | dt calculations, physics | | **Audio Playback Time** | What's being HEARD | `audio_get_playback_time()` | **Audio-visual sync, beat display** | | **Music Time** | Tracker time (tempo-scaled) | `g_music_time` | Tracker event triggering | --- ## Correct Implementation Example This demonstrates how `test_demo.cc` and `main.cc` *should* be implemented to achieve proper synchronization. ### Before (Current Incorrect State ❌) ```cpp const double current_time = platform_state.time; // Physical time // Beat calculation based on physical time const float beat_time = (float)current_time * 120.0f / 60.0f; // But peak is measured at audio playback time (e.g. 400ms behind!) const float raw_peak = audio_get_realtime_peak(); // MISMATCH: beat and peak are from different time sources! ``` **Problem:** Visual beat shows beat 2 (from physical time), but peak is for beat 1 (from audio playback time). ### After (Proposed Correct State ✅) ```cpp // Audio playback time: what's being HEARD right now const float audio_time = audio_get_playback_time(); // Beat calculation uses AUDIO TIME (matches peak measurement) const float beat_time = audio_time * 120.0f / 60.0f; // Peak is measured at audio playback time const float raw_peak = audio_get_realtime_peak(); // SYNCHRONIZED: beat and peak are from the same time source! ``` **Result:** Visual beat shows beat 1, peak is for beat 1 → synchronized! ✅ --- ## How audio_get_playback_time() Works **Implementation** (audio.cc:169-173): ```cpp float audio_get_playback_time() { const int64_t total_samples = g_ring_buffer.get_total_read(); return (float)total_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); } ``` **Key Points:** 1. **Tracks samples consumed** by the audio callback (what is being heard). 2. **Automatically accounts for ring buffer latency** without hardcoded constants. 3. **Is self-consistent** with `audio_get_realtime_peak()`, which is measured at the same moment. --- ## Implementation Plan (Task #71) To fix the jitter and simplify the audio pipeline, the following steps should be taken. ### Phase 1: Fix Core Timing Synchronization 1. **Update `main.cc` and `test_demo.cc`:** Modify the main loop and test loop to use `audio_get_playback_time()` as the time source for all beat and visual calculations. This is the highest priority fix. ### Phase 2: Implement Data-Driven Configuration 1. **Create `tracker_get_bpm()` API:** * **Purpose:** Provide a formal API to read the BPM from the active `.track` file, removing the need for hardcoded values. * **Implementation:** ```cpp // In tracker.h: float tracker_get_bpm(); // In tracker.cc: float tracker_get_bpm() { return g_tracker_score.bpm; } ``` * **Adoption:** Update `main.cc` and `test_demo.cc` to call this function instead of using `g_tracker_score.bpm` directly or hardcoding `120.0f`. 2. **Implement BPM-Aware Peak Decay Rate:** * **Purpose:** Calculate the peak decay rate dynamically based on the current BPM, so that visual feedback feels consistent across different tempos. * **Implementation:** ```cpp // In audio.h (or similar): float audio_get_peak_decay_rate(); // In audio.cc: float audio_get_peak_decay_rate() { const float bpm = tracker_get_bpm(); const float beat_interval = 60.0f / bpm; // e.g., 0.5s at 120 BPM const float callback_interval = 0.128f; // Approx. from device const float num_callbacks_per_beat = beat_interval / callback_interval; // Decay to 10% within one beat return powf(0.1f, 1.0f / num_callbacks_per_beat); } // In miniaudio_backend.cc: // Replace realtime_peak_ *= 0.5f; with: realtime_peak_ *= audio_get_peak_decay_rate(); ``` ### Phase 3: Architectural Simplification 1. **Implement a `TimeProvider` Class:** * **Purpose:** Centralize all timing queries into a single, authoritative source to simplify the logic in the main loop and effects system. * **Design:** ```cpp // In a new file, e.g., audio/time_provider.h: class TimeProvider { public: // Returns current time for AV sync (what's being heard) float get_current_time() const { return audio_get_playback_time(); } // Returns current beat (fractional, BPM-aware) float get_current_beat() const; // Returns current peak (synchronized with current time) float get_current_peak() const { return audio_get_realtime_peak(); } // Returns current BPM float get_bpm() const { return tracker_get_bpm(); } }; ``` * **Migration:** Gradually refactor `main.cc`, `test_demo.cc`, and visual effects to get all timing information from an instance of this class, removing the need to pass time, beat, and peak as parameters. --- ### Design Principles to Uphold 1. ✅ **Single physical clock:** `platform_get_time()` is the only wall clock. 2. ✅ **Systems expose their state:** `audio_get_playback_time()` reports its true playback position. 3. ✅ **No hardcoded constants:** System queries its own state dynamically. 4. ✅ **Data-driven configuration:** BPM comes from the tracker, decay rate comes from BPM. 5. ✅ **Synchronized time sources:** Beat calculations and peak measurements must use the same clock (`audio_get_playback_time`).