summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GEMINI.md7
-rw-r--r--PROJECT_CONTEXT.md2
-rw-r--r--TODO.md18
-rw-r--r--peaks.txt34
-rw-r--r--peaks_fixed.txt38
-rw-r--r--src/audio/audio_engine.cc8
-rw-r--r--src/audio/audio_engine.h2
-rw-r--r--src/audio/synth.cc4
-rw-r--r--src/audio/synth.h1
-rw-r--r--src/audio/tracker.cc36
-rw-r--r--src/audio/tracker.h2
-rw-r--r--src/main.cc69
-rw-r--r--src/test_demo.cc31
-rw-r--r--src/tests/test_jittered_audio.cc4
-rw-r--r--src/tests/test_tracker.cc8
-rw-r--r--src/tests/test_tracker_timing.cc20
-rw-r--r--src/tests/test_variable_tempo.cc24
-rw-r--r--src/tests/test_wav_dump.cc2
18 files changed, 128 insertions, 182 deletions
diff --git a/GEMINI.md b/GEMINI.md
index f8524e0..23b7034 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -68,6 +68,9 @@ IMPORTANT:
- Do NOT modify files outside the current scope
- Do NOT perform refactors or cleanups unless explicitly asked
- **Always use `-j4` for all `cmake --build` commands.**
+- Concise answers only
+- No explanations unless asked
+- Max 100 tokens per reply
# Context Maintenance:
- See @doc/CONTEXT_MAINTENANCE.md for keeping context clean
@@ -130,7 +133,3 @@ IMPORTANT:
</task_state>
</state_snapshot>
--- End of Context from: GEMINI.md ---
-Rules:
-- Concise answers only
-- No explanations unless asked
-- Max 100 tokens per reply \ No newline at end of file
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index 11f2f4b..9b36223 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -33,7 +33,7 @@ Style:
**Note:** For detailed history of recently completed milestones, see `COMPLETED.md`.
### Current Status
-- Audio system: Stable with real-time peak tracking, variable tempo support, comprehensive test coverage
+- Audio system: Sample-accurate synchronization achieved. Uses hardware playback time as master clock. Variable tempo support integrated. Comprehensive test coverage maintained.
- Build system: Optimized with proper asset dependency tracking
- Shader system: Modular with comprehensive compilation tests
- 3D rendering: Hybrid SDF/rasterization with BVH acceleration and binary scene loader
diff --git a/TODO.md b/TODO.md
index a6ebe3e..2400113 100644
--- a/TODO.md
+++ b/TODO.md
@@ -4,20 +4,22 @@ This file tracks prioritized tasks with detailed attack plans.
**Note:** For a history of recently completed tasks, see `COMPLETED.md`.
-## Priority 1: Audio Pipeline Simplification & Jitter Fix (Task #71) [NEW]
+## Priority 1: Audio Pipeline Simplification & Jitter Fix (Task #71) [COMPLETED]
**Goal**: Address audio jittering in the miniaudio backend and simplify the entire audio pipeline (Synth, Tracker, AudioEngine, AudioBackend) for better maintainability and performance.
+**Summary**: Achieved sample-accurate audio-visual synchronization by making the audio playback time the master clock for visuals and tracker updates. Eliminated jitter by using a stable audio clock for scheduling. See HANDOFF_2026-02-07_Final.md for details.
+
### Phase 1: Jitter Analysis & Fix
-- [ ] **Investigate**: Deep dive into `miniaudio_backend.cc` to find the root cause of audio jitter. Analyze buffer sizes, callback timing, and thread synchronization.
-- [ ] **Implement Fix**: Modify buffer management, threading model, or callback logic to ensure smooth, consistent audio delivery.
-- [ ] **Verify**: Create a new, specific test case in `src/tests/test_audio_backend.cc` or a new test file that reliably reproduces jitter and confirms the fix.
+- [x] **Investigate**: Deep dive into `miniaudio_backend.cc` to find the root cause of audio jitter. Analyze buffer sizes, callback timing, and thread synchronization.
+- [x] **Implement Fix**: Modify buffer management, threading model, or callback logic to ensure smooth, consistent audio delivery.
+- [x] **Verify**: Create a new, specific test case in `src/tests/test_audio_backend.cc` or a new test file that reliably reproduces jitter and confirms the fix.
### Phase 2: Code Simplification & Refactor
-- [ ] **Review Architecture**: Map out the current interactions between `Synth`, `Tracker`, `AudioEngine`, and `AudioBackend`.
-- [ ] **Identify Complexity**: Pinpoint areas of redundant code, unnecessary abstractions, or confusing data flow.
-- [ ] **Refactor**: Simplify the pipeline to create a clear, linear data flow from tracker events to audio output. Reduce dependencies and clarify ownership of resources.
-- [ ] **Update Documentation**: Modify `doc/HOWTO.md` and `doc/CONTRIBUTING.md` to reflect the new, simpler audio architecture.
+- [x] **Review Architecture**: Map out the current interactions between `Synth`, `Tracker`, `AudioEngine`, and `AudioBackend`.
+- [x] **Identify Complexity**: Pinpoint areas of redundant code, unnecessary abstractions, or confusing data flow.
+- [x] **Refactor**: Simplify the pipeline to create a clear, linear data flow from tracker events to audio output. Reduce dependencies and clarify ownership of resources.
+- [x] **Update Documentation**: Modify `doc/HOWTO.md` and `doc/CONTRIBUTING.md` to reflect the new, simpler audio architecture.
---
diff --git a/peaks.txt b/peaks.txt
deleted file mode 100644
index 3fdd9ec..0000000
--- a/peaks.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-# Audio peak log from test_demo
-# Mode: beat-aligned
-# To plot with gnuplot:
-# gnuplot -p -e "set xlabel 'Time (s)'; set ylabel 'Peak'; plot 'peaks.txt' using 2:3 with lines title 'Raw Peak'"
-# Columns: beat_number clock_time raw_peak
-#
-0 0.189516 0.588233
-1 0.502906 0.177229
-2 1.003406 0.235951
-3 1.502298 0.199312
-4 2.002919 0.234061
-5 2.503558 0.475179
-6 3.002598 0.334373
-7 3.503073 0.199128
-8 4.003374 0.234061
-9 4.503743 0.975382
-10 5.002930 0.272136
-11 5.504852 0.204941
-12 6.003064 0.234083
-13 6.503076 0.475188
-14 7.002489 0.234061
-15 7.503902 0.199286
-16 8.002816 0.334373
-17 8.502699 0.475188
-18 9.002795 0.234061
-19 9.503774 0.199128
-20 10.003943 0.234061
-21 10.503923 0.412922
-22 11.002934 0.285239
-23 11.502814 0.199328
-24 12.002732 0.238938
-25 12.502844 0.975236
-26 13.003447 0.388766
-27 13.503064 0.204941
diff --git a/peaks_fixed.txt b/peaks_fixed.txt
deleted file mode 100644
index 2e111d4..0000000
--- a/peaks_fixed.txt
+++ /dev/null
@@ -1,38 +0,0 @@
-# Audio peak log from test_demo
-# Mode: beat-aligned
-# To plot with gnuplot:
-# gnuplot -p -e "set xlabel 'Time (s)'; set ylabel 'Peak'; plot 'peaks_fixed.txt' using 2:3 with lines title 'Raw Peak'"
-# Columns: beat_number clock_time raw_peak
-#
-0 0.064000 0.588233
-1 0.512000 0.158621
-2 1.024000 0.039656
-3 1.504000 0.074429
-4 2.016000 0.039339
-5 2.528000 0.203162
-6 3.008000 0.039339
-7 3.520000 0.052100
-8 4.000000 0.039339
-9 4.512000 0.785926
-10 5.024000 0.112183
-11 5.504000 0.078633
-12 6.016000 0.039342
-13 6.528000 0.290565
-14 7.008000 0.047940
-15 7.520000 0.074038
-16 8.000000 0.040158
-17 8.512000 0.290565
-18 9.024000 0.033558
-19 9.504000 0.074038
-20 10.016000 0.028111
-21 10.528000 0.203395
-22 11.008000 0.039339
-23 11.520000 0.074038
-24 12.000000 0.040158
-25 12.512000 0.785926
-26 13.024000 0.116352
-27 13.504000 0.078280
-28 14.016000 0.039342
-29 14.528000 0.203162
-30 15.008000 0.039339
-31 15.520000 0.052100
diff --git a/src/audio/audio_engine.cc b/src/audio/audio_engine.cc
index 6d2ee92..d11303c 100644
--- a/src/audio/audio_engine.cc
+++ b/src/audio/audio_engine.cc
@@ -82,14 +82,14 @@ void AudioEngine::load_music_data(const TrackerScore* score,
#endif
}
-void AudioEngine::update(float music_time) {
+void AudioEngine::update(float music_time, float dt) {
current_time_ = music_time;
// Pre-warm samples needed in next 2 seconds (lazy loading strategy)
// TODO: Implement pre-warming based on upcoming pattern triggers
// Update tracker (triggers events)
- tracker_update(music_time);
+ tracker_update(music_time, dt);
}
void AudioEngine::render(float* output_buffer, int num_frames) {
@@ -191,7 +191,7 @@ void AudioEngine::seek(float target_time) {
}
// 6. Final update at exact target time
- tracker_update(target_time);
+ tracker_update(target_time, 0.0f);
current_time_ = target_time;
#if defined(DEBUG_LOG_AUDIO)
@@ -216,6 +216,6 @@ void AudioEngine::update_silent(float music_time) {
// Update tracker without triggering audio (for fast-forward/seeking)
// This is a placeholder - proper implementation requires tracker support
// for silent updates. For now, we just update normally.
- tracker_update(music_time);
+ tracker_update(music_time, 0.0f);
}
#endif /* !defined(STRIP_ALL) */
diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h
index 95761ad..699213d 100644
--- a/src/audio/audio_engine.h
+++ b/src/audio/audio_engine.h
@@ -21,7 +21,7 @@ class AudioEngine {
const AssetId* sample_assets, uint32_t sample_count);
// Update loop
- void update(float music_time);
+ void update(float music_time, float dt);
#if !defined(STRIP_ALL)
// Timeline seeking (debugging only)
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index e790c12..5fadf3c 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -67,6 +67,10 @@ void synth_set_tempo_scale(float tempo_scale) {
g_tempo_scale = tempo_scale;
}
+float synth_get_tempo_scale() {
+ return g_tempo_scale;
+}
+
int synth_register_spectrogram(const Spectrogram* spec) {
#if defined(DEBUG_LOG_SYNTH)
// VALIDATION: Check spectrogram pointer and data
diff --git a/src/audio/synth.h b/src/audio/synth.h
index b2625b3..3a42a61 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -43,6 +43,7 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan,
void synth_render(float* output_buffer, int num_frames);
void synth_set_tempo_scale(
float tempo_scale); // Set playback speed (1.0 = normal)
+float synth_get_tempo_scale();
int synth_get_active_voice_count();
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 2bb4159..42e074d 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -215,19 +215,22 @@ static void trigger_note_event(const TrackerEvent& event,
start_offset_samples);
}
-void tracker_update(float music_time_sec) {
+void tracker_update(float music_time_sec, float dt_music_sec) {
// Unit-less timing: 1 unit = 4 beats (by convention)
const float BEATS_PER_UNIT = 4.0f;
const float unit_duration_sec =
(BEATS_PER_UNIT / g_tracker_score.bpm) * 60.0f;
+ const float end_music_time = music_time_sec + dt_music_sec;
+ const float tempo_scale = synth_get_tempo_scale();
+
// Step 1: Process new pattern triggers
while (g_last_trigger_idx < g_tracker_score.num_triggers) {
const TrackerPatternTrigger& trigger =
g_tracker_score.triggers[g_last_trigger_idx];
const float trigger_time_sec = trigger.unit_time * unit_duration_sec;
- if (trigger_time_sec > music_time_sec)
+ if (trigger_time_sec > end_music_time)
break;
// Add this pattern to active patterns list
@@ -243,11 +246,7 @@ void tracker_update(float music_time_sec) {
}
// Step 2: Update all active patterns and trigger individual events
- // NOTE: We trigger events immediately when their time passes (no sample
- // offsets) This gives ~16ms quantization (60fps) which is acceptable Sample
- // offsets don't work with tempo scaling because music_time and render_time
- // are in different time domains (tempo-scaled vs physical)
-
+ // Sample-accurate timing: Calculate offset relative to music_time_sec
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
if (!g_active_patterns[i].active)
continue;
@@ -255,26 +254,31 @@ void tracker_update(float music_time_sec) {
ActivePattern& active = g_active_patterns[i];
const TrackerPattern& pattern = g_tracker_patterns[active.pattern_id];
- // Calculate elapsed unit-less time since pattern started
- const float elapsed_music_time = music_time_sec - active.start_music_time;
- const float elapsed_units = elapsed_music_time / unit_duration_sec;
-
// Trigger all events that have passed their unit time
while (active.next_event_idx < pattern.num_events) {
const TrackerEvent& event = pattern.events[active.next_event_idx];
+ const float event_music_time =
+ active.start_music_time + event.unit_time * unit_duration_sec;
- if (event.unit_time > elapsed_units)
+ if (event_music_time > end_music_time)
break; // This event hasn't reached its time yet
- // Trigger this event immediately (no sample offset)
- // Timing quantization: ~16ms at 60fps, acceptable for rhythm
- trigger_note_event(event, 0);
+ // Sample-accurate timing:
+ // Offset = (music_time_delta / tempo_scale) * sample_rate
+ int sample_offset = 0;
+ if (event_music_time > music_time_sec) {
+ sample_offset = (int)((event_music_time - music_time_sec) /
+ tempo_scale * 32000.0f);
+ }
+ trigger_note_event(event, sample_offset);
active.next_event_idx++;
}
// Pattern remains active until full duration elapses
- if (elapsed_units >= pattern.unit_length) {
+ const float pattern_end_time =
+ active.start_music_time + pattern.unit_length * unit_duration_sec;
+ if (pattern_end_time <= end_music_time) {
active.active = false;
}
}
diff --git a/src/audio/tracker.h b/src/audio/tracker.h
index 3ef06a1..8e7a63f 100644
--- a/src/audio/tracker.h
+++ b/src/audio/tracker.h
@@ -42,5 +42,5 @@ extern const uint32_t g_tracker_patterns_count;
extern const TrackerScore g_tracker_score;
void tracker_init();
-void tracker_update(float music_time_sec);
+void tracker_update(float music_time_sec, float dt_music_sec);
void tracker_reset(); // Reset tracker state (for tests/seeking)
diff --git a/src/main.cc b/src/main.cc
index 32f3c99..51060ce 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -89,9 +89,9 @@ int main(int argc, char** argv) {
// Music time state for variable tempo
static float g_music_time = 0.0f;
static float g_tempo_scale = 1.0f; // 1.0 = normal speed
- static double g_last_physical_time = 0.0;
+ static float g_last_audio_time = 0.0f;
- auto fill_audio_buffer = [&](double t) {
+ auto fill_audio_buffer = [&](float audio_dt, double physical_time) {
// Variable tempo system - acceleration phases for demo effect
// Phase 1 (0-5s): Steady 1.0x
// Phase 2 (5-10s): Steady 1.0x
@@ -100,17 +100,17 @@ int main(int argc, char** argv) {
// Phase 5 (20-25s): Decelerate from 1.0x to 0.5x
// Phase 6 (25s+): Steady 1.0x (reset after deceleration)
const float prev_tempo = g_tempo_scale;
- if (t < 10.0) {
+ if (physical_time < 10.0) {
g_tempo_scale = 1.0f; // Steady at start
- } else if (t < 15.0) {
+ } else if (physical_time < 15.0) {
// Phase 3: Linear acceleration
- const float progress = (float)(t - 10.0) / 5.0f;
+ const float progress = (float)(physical_time - 10.0) / 5.0f;
g_tempo_scale = 1.0f + progress * 1.0f; // 1.0 → 2.0
- } else if (t < 20.0) {
+ } else if (physical_time < 20.0) {
g_tempo_scale = 1.0f; // Reset to normal
- } else if (t < 25.0) {
+ } else if (physical_time < 25.0) {
// Phase 5: Linear deceleration
- const float progress = (float)(t - 20.0) / 5.0f;
+ const float progress = (float)(physical_time - 20.0) / 5.0f;
g_tempo_scale = 1.0f - progress * 0.5f; // 1.0 → 0.5
} else {
g_tempo_scale = 1.0f; // Reset to normal
@@ -120,29 +120,25 @@ int main(int argc, char** argv) {
#if !defined(STRIP_ALL)
// Debug output when tempo changes significantly
if (fabsf(g_tempo_scale - prev_tempo) > 0.05f) {
- printf("[Tempo] t=%.2fs, tempo=%.3fx, music_time=%.3fs\n", (float)t,
- g_tempo_scale, g_music_time);
+ printf("[Tempo] t=%.2fs, tempo=%.3fx, music_time=%.3fs\n",
+ (float)physical_time, g_tempo_scale, g_music_time);
}
#endif
- // Calculate delta time
- const float dt = (float)(t - g_last_physical_time);
- g_last_physical_time = t;
-
// CRITICAL: Update tracker BEFORE advancing music_time
// This ensures events trigger in the correct frame, not one frame early
// Pass current music_time (not future time) to tracker
- g_audio_engine.update(g_music_time);
+ g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale);
// Fill ring buffer with upcoming audio (look-ahead rendering)
// CRITICAL: Scale dt by tempo to render enough audio during
// acceleration/deceleration At 2.0x tempo, we consume 2x audio per physical
// second, so we must render 2x per frame
- audio_render_ahead(g_music_time, dt * g_tempo_scale);
+ audio_render_ahead(g_music_time, audio_dt * g_tempo_scale);
// Advance music time AFTER rendering audio for this frame
// This prevents events from triggering one frame early
- g_music_time += dt * g_tempo_scale;
+ g_music_time += audio_dt * g_tempo_scale;
};
#if !defined(STRIP_ALL)
@@ -153,7 +149,7 @@ int main(int argc, char** argv) {
// We step at ~60hz
const double step = 1.0 / 60.0;
for (double t = 0.0; t < seek_time; t += step) {
- fill_audio_buffer(t);
+ fill_audio_buffer(step, t);
audio_render_silent((float)step);
}
@@ -164,11 +160,13 @@ int main(int argc, char** argv) {
// PRE-FILL: Fill ring buffer with initial 200ms before starting audio device
// This prevents underrun on first callback
- g_audio_engine.update(g_music_time);
- audio_render_ahead(g_music_time, 1.0f / 60.0f); // Fill buffer with lookahead
+ g_audio_engine.update(g_music_time, 1.0f / 60.0f);
+ audio_render_ahead(g_music_time,
+ 1.0f / 60.0f); // Fill buffer with lookahead
// Start audio (or render to WAV file)
audio_start();
+ g_last_audio_time = audio_get_playback_time(); // Initialize after start
#if !defined(STRIP_ALL)
// In WAV dump mode, run headless simulation and write audio to file
@@ -177,7 +175,7 @@ int main(int argc, char** argv) {
const float demo_duration = GetDemoDuration();
const float max_duration = (demo_duration > 0.0f) ? demo_duration : 60.0f;
- const float update_dt = 1.0f / 60.0f; // 60Hz update rate
+ const float update_dt = 1.0f / 60.0f; // 60Hz update rate
const int frames_per_update = (int)(32000 * update_dt); // ~533 frames
const int samples_per_update = frames_per_update * 2; // Stereo
@@ -188,7 +186,7 @@ int main(int argc, char** argv) {
while (physical_time < max_duration) {
// Update music time and tracker (using tempo logic from
// fill_audio_buffer)
- fill_audio_buffer(physical_time);
+ fill_audio_buffer(update_dt, physical_time);
// Read rendered audio from ring buffer
if (ring_buffer != nullptr) {
@@ -235,18 +233,23 @@ int main(int argc, char** argv) {
gpu_resize(last_width, last_height);
}
- const double current_time =
+ const double physical_time =
platform_state.time + seek_time; // Offset logic time
// Auto-exit when demo finishes (if duration is specified)
- if (demo_duration > 0.0f && current_time >= demo_duration) {
+ if (demo_duration > 0.0f && physical_time >= demo_duration) {
#if !defined(STRIP_ALL)
- printf("Demo finished at %.2f seconds. Exiting...\n", current_time);
+ printf("Demo finished at %.2f seconds. Exiting...\n", physical_time);
#endif
break;
}
- fill_audio_buffer(current_time);
+ // Calculate stable audio dt for jitter-free tracker updates
+ const float audio_time = audio_get_playback_time();
+ const float audio_dt = audio_time - g_last_audio_time;
+ g_last_audio_time = audio_time;
+
+ fill_audio_buffer(audio_dt, physical_time);
const float aspect_ratio = platform_state.aspect_ratio;
@@ -256,21 +259,23 @@ int main(int argc, char** argv) {
const float visual_peak = fminf(raw_peak * 8.0f, 1.0f);
// Calculate beat information for synchronization
- const float beat_time = (float)current_time * g_tracker_score.bpm / 60.0f;
+ // MASTER CLOCK: Use audio playback time for perfect visual sync
+ const float sync_time = audio_get_playback_time();
+ const float beat_time = sync_time * g_tracker_score.bpm / 60.0f;
const int beat_number = (int)beat_time;
const float beat = fmodf(beat_time, 1.0f); // Fractional part (0.0 to 1.0)
#if !defined(STRIP_ALL)
// Print beat/time info periodically for identifying sync points
static float last_print_time = -1.0f;
- if (current_time - last_print_time >= 0.5f) { // Print every 0.5 seconds
- printf("[T=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f]\n", (float)current_time,
- beat_number, beat, visual_peak);
- last_print_time = (float)current_time;
+ if (sync_time - last_print_time >= 0.5f) { // Print every 0.5 seconds
+ printf("[T=%.2f, MusicT=%.2f, Beat=%d, Frac=%.2f, Peak=%.2f]\n",
+ sync_time, g_music_time, beat_number, beat, visual_peak);
+ last_print_time = sync_time;
}
#endif /* !defined(STRIP_ALL) */
- gpu_draw(visual_peak, aspect_ratio, (float)current_time, beat);
+ gpu_draw(visual_peak, aspect_ratio, sync_time, beat);
audio_update();
}
diff --git a/src/test_demo.cc b/src/test_demo.cc
index 8209eca..bb92446 100644
--- a/src/test_demo.cc
+++ b/src/test_demo.cc
@@ -219,20 +219,18 @@ int main(int argc, char** argv) {
// Music time tracking with optional tempo variation
static float g_music_time = 0.0f;
- static double g_last_physical_time = 0.0;
+ static float g_last_audio_time = 0.0f;
static float g_tempo_scale = 1.0f;
- auto fill_audio_buffer = [&](double t) {
- const float dt = (float)(t - g_last_physical_time);
- g_last_physical_time = t;
-
+ auto fill_audio_buffer = [&](float audio_dt, double physical_time) {
// Calculate tempo scale if --tempo flag enabled
if (tempo_test_enabled) {
// Each bar = 2 seconds at 120 BPM (4 beats)
const float bar_duration = 2.0f;
- const int bar_number = (int)(t / bar_duration);
+ const int bar_number = (int)(physical_time / bar_duration);
const float bar_progress =
- fmodf((float)t, bar_duration) / bar_duration; // 0.0-1.0 within bar
+ fmodf((float)physical_time, bar_duration) /
+ bar_duration; // 0.0-1.0 within bar
if (bar_number % 2 == 0) {
// Even bars: Ramp from 1.0x → 1.5x
@@ -245,16 +243,17 @@ int main(int argc, char** argv) {
g_tempo_scale = 1.0f; // No tempo variation
}
- g_music_time += dt * g_tempo_scale;
+ g_music_time += audio_dt * g_tempo_scale;
- g_audio_engine.update(g_music_time);
- audio_render_ahead(g_music_time, dt * g_tempo_scale);
+ g_audio_engine.update(g_music_time, audio_dt * g_tempo_scale);
+ audio_render_ahead(g_music_time, audio_dt * g_tempo_scale);
};
// Pre-fill audio buffer
- g_audio_engine.update(g_music_time);
+ g_audio_engine.update(g_music_time, 1.0f / 60.0f);
audio_render_ahead(g_music_time, 1.0f / 60.0f);
audio_start();
+ g_last_audio_time = audio_get_playback_time();
int last_width = platform_state.width;
int last_height = platform_state.height;
@@ -312,13 +311,17 @@ int main(int argc, char** argv) {
break;
}
- fill_audio_buffer(physical_time);
+ // Calculate stable audio dt for jitter-free tracker updates
+ const float audio_time = audio_get_playback_time();
+ const float audio_dt = audio_time - g_last_audio_time;
+ g_last_audio_time = audio_time;
+
+ fill_audio_buffer(audio_dt, physical_time);
// Audio-visual synchronization: Use audio playback time (not physical
// time!) This accounts for ring buffer latency automatically (no hardcoded
// constants)
- const float audio_time = audio_get_playback_time();
-
+
// Audio/visual sync parameters
const float aspect_ratio = platform_state.aspect_ratio;
// Peak is measured at audio playback time, so it matches audio_time
diff --git a/src/tests/test_jittered_audio.cc b/src/tests/test_jittered_audio.cc
index 8afb8c0..cad0da4 100644
--- a/src/tests/test_jittered_audio.cc
+++ b/src/tests/test_jittered_audio.cc
@@ -45,7 +45,7 @@ void test_jittered_audio_basic() {
music_time += dt; // Normal tempo
// Update tracker and fill buffer
- tracker_update(music_time);
+ tracker_update(music_time, dt);
audio_render_ahead(music_time, dt);
// Sleep to simulate frame time
@@ -113,7 +113,7 @@ void test_jittered_audio_with_acceleration() {
music_time += dt * tempo_scale;
// Update tracker and fill buffer
- tracker_update(music_time);
+ tracker_update(music_time, dt * tempo_scale);
audio_render_ahead(music_time, dt);
// Sleep to simulate frame time
diff --git a/src/tests/test_tracker.cc b/src/tests/test_tracker.cc
index 8265903..6be2a8d 100644
--- a/src/tests/test_tracker.cc
+++ b/src/tests/test_tracker.cc
@@ -37,13 +37,13 @@ void test_tracker_pattern_triggering() {
// drums_basic:
// 0.00, ASSET_KICK_1
// 0.00, NOTE_A4
- engine.update(0.0f);
+ engine.update(0.0f, 0.0f);
// Expect 2 voices: kick + note
assert(engine.get_active_voice_count() == 2);
// Test 2: At music_time = 0.25f (beat 0.5 @ 120 BPM), snare event triggers
// 0.25, ASSET_SNARE_1
- engine.update(0.25f);
+ engine.update(0.25f, 0.0f);
// Expect at least 2 voices (snare + maybe others)
// Exact count depends on sample duration (kick/note might have finished)
int voices = engine.get_active_voice_count();
@@ -51,12 +51,12 @@ void test_tracker_pattern_triggering() {
// Test 3: At music_time = 0.5f (beat 1.0), kick event triggers
// 0.50, ASSET_KICK_1
- engine.update(0.5f);
+ engine.update(0.5f, 0.0f);
// Expect at least 3 voices (new kick + others)
assert(engine.get_active_voice_count() >= 3);
// Test 4: Advance to 2.0f - new patterns trigger at time 2.0f
- engine.update(2.0f);
+ engine.update(2.0f, 0.0f);
// Many events have triggered by now
assert(engine.get_active_voice_count() > 5);
diff --git a/src/tests/test_tracker_timing.cc b/src/tests/test_tracker_timing.cc
index 5c0e9bf..a279c8e 100644
--- a/src/tests/test_tracker_timing.cc
+++ b/src/tests/test_tracker_timing.cc
@@ -66,7 +66,7 @@ void test_basic_event_recording() {
engine.init();
// Trigger at t=0.0 (should trigger initial patterns)
- engine.update(0.0f);
+ engine.update(0.0f, 0.0f);
const auto& events = backend.get_events();
printf(" Events triggered at t=0.0: %zu\n", events.size());
@@ -93,17 +93,17 @@ void test_progressive_triggering() {
engine.init();
// Update at t=0
- engine.update(0.0f);
+ engine.update(0.0f, 0.0f);
const size_t events_at_0 = backend.get_events().size();
printf(" Events at t=0.0: %zu\n", events_at_0);
// Update at t=1.0
- engine.update(1.0f);
+ engine.update(1.0f, 0.0f);
const size_t events_at_1 = backend.get_events().size();
printf(" Events at t=1.0: %zu\n", events_at_1);
// Update at t=2.0
- engine.update(2.0f);
+ engine.update(2.0f, 0.0f);
const size_t events_at_2 = backend.get_events().size();
printf(" Events at t=2.0: %zu\n", events_at_2);
@@ -126,7 +126,7 @@ void test_simultaneous_triggers() {
// Clear and update to first trigger point
backend.clear_events();
- engine.update(0.0f);
+ engine.update(0.0f, 0.0f);
const auto& events = backend.get_events();
if (events.size() == 0) {
@@ -174,7 +174,7 @@ void test_timing_monotonicity() {
// Update through several time points
for (float t = 0.0f; t <= 5.0f; t += 0.5f) {
- engine.update(t);
+ engine.update(t, 0.5f);
}
const auto& events = backend.get_events();
@@ -207,7 +207,7 @@ void test_seek_simulation() {
float t = 0.0f;
const float step = 0.1f;
while (t <= seek_target) {
- engine.update(t);
+ engine.update(t, step);
// Simulate audio rendering
float dummy_buffer[512 * 2];
engine.render(dummy_buffer, 512);
@@ -244,7 +244,7 @@ void test_timestamp_clustering() {
// Update through the first 4 seconds
for (float t = 0.0f; t <= 4.0f; t += 0.1f) {
- engine.update(t);
+ engine.update(t, 0.1f);
}
const auto& events = backend.get_events();
@@ -277,7 +277,7 @@ void test_render_integration() {
engine.init();
// Trigger some patterns
- engine.update(0.0f);
+ engine.update(0.0f, 0.0f);
const size_t events_before = backend.get_events().size();
// Render 1 second of silent audio
@@ -289,7 +289,7 @@ void test_render_integration() {
assert(backend_time >= 0.9f && backend_time <= 1.1f);
// Trigger more patterns after time advance
- engine.update(1.0f);
+ engine.update(1.0f, 0.0f);
const size_t events_after = backend.get_events().size();
printf(" Events before: %zu, after: %zu\n", events_before, events_after);
diff --git a/src/tests/test_variable_tempo.cc b/src/tests/test_variable_tempo.cc
index e27e7d6..4fc81e3 100644
--- a/src/tests/test_variable_tempo.cc
+++ b/src/tests/test_variable_tempo.cc
@@ -44,7 +44,7 @@ void test_basic_tempo_scaling() {
for (int i = 0; i < 10; ++i) {
float dt = 0.1f; // 100ms physical steps
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
// After 1 second physical time at 1.0x tempo:
@@ -64,7 +64,7 @@ void test_basic_tempo_scaling() {
for (int i = 0; i < 10; ++i) {
float dt = 0.1f;
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
// After 1 second physical time at 2.0x tempo:
@@ -84,7 +84,7 @@ void test_basic_tempo_scaling() {
for (int i = 0; i < 10; ++i) {
float dt = 0.1f;
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
// After 1 second physical time at 0.5x tempo:
@@ -123,7 +123,7 @@ void test_2x_speedup_reset_trick() {
tempo_scale = fminf(tempo_scale, 2.0f);
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
@@ -142,7 +142,7 @@ void test_2x_speedup_reset_trick() {
for (int i = 0; i < 20; ++i) {
physical_time += dt;
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
printf(" After reset + 2s: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
@@ -183,7 +183,7 @@ void test_2x_slowdown_reset_trick() {
tempo_scale = fmaxf(tempo_scale, 0.5f);
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
printf(" After 5s physical: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
@@ -201,7 +201,7 @@ void test_2x_slowdown_reset_trick() {
for (int i = 0; i < 20; ++i) {
physical_time += dt;
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
}
printf(" After reset + 2s: tempo=%.2fx, music_time=%.3f\n", tempo_scale,
@@ -235,7 +235,7 @@ void test_pattern_density_swap() {
printf(" Phase 1: Sparse pattern, normal tempo\n");
for (float t = 0.0f; t < 3.0f; t += 0.1f) {
music_time += 0.1f * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, 0.1f * tempo_scale);
}
const size_t sparse_events = backend.get_events().size();
printf(" Events during sparse phase: %zu\n", sparse_events);
@@ -245,7 +245,7 @@ void test_pattern_density_swap() {
tempo_scale = 2.0f;
for (float t = 0.0f; t < 2.0f; t += 0.1f) {
music_time += 0.1f * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, 0.1f * tempo_scale);
}
const size_t events_at_2x = backend.get_events().size() - sparse_events;
printf(" Additional events during 2.0x: %zu\n", events_at_2x);
@@ -260,7 +260,7 @@ void test_pattern_density_swap() {
const size_t events_before_reset_phase = backend.get_events().size();
for (float t = 0.0f; t < 2.0f; t += 0.1f) {
music_time += 0.1f * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, 0.1f * tempo_scale);
}
const size_t events_after_reset = backend.get_events().size();
@@ -304,7 +304,7 @@ void test_continuous_acceleration() {
tempo_scale = fmaxf(min_tempo, fminf(max_tempo, tempo_scale));
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
// Log at key points
if (i % 50 == 0) {
@@ -354,7 +354,7 @@ void test_oscillating_tempo() {
float tempo_scale = 1.0f + 0.2f * sinf(physical_time * 2.0f);
music_time += dt * tempo_scale;
- engine.update(music_time);
+ engine.update(music_time, dt * tempo_scale);
if (i % 25 == 0) {
printf(" t=%.2fs: tempo=%.3fx, music_time=%.3f\n", physical_time,
diff --git a/src/tests/test_wav_dump.cc b/src/tests/test_wav_dump.cc
index 880c8cd..eb14652 100644
--- a/src/tests/test_wav_dump.cc
+++ b/src/tests/test_wav_dump.cc
@@ -59,7 +59,7 @@ void test_wav_format_matches_live_audio() {
float music_time = 0.0f;
for (float t = 0.0f; t < duration; t += update_dt) {
// Update audio engine (triggers patterns)
- engine.update(music_time);
+ engine.update(music_time, update_dt);
music_time += update_dt;
// Render audio ahead