summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/AUDIO_WAV_DRIFT_BUG.md17
-rw-r--r--src/app/main.cc41
-rw-r--r--src/audio/audio.cc17
-rw-r--r--src/audio/tracker.cc8
-rw-r--r--tools/timeline_editor/index.html4
-rw-r--r--tools/timeline_editor/timeline-playback.js2
6 files changed, 41 insertions, 48 deletions
diff --git a/doc/AUDIO_WAV_DRIFT_BUG.md b/doc/AUDIO_WAV_DRIFT_BUG.md
index e22f4fa..050dd49 100644
--- a/doc/AUDIO_WAV_DRIFT_BUG.md
+++ b/doc/AUDIO_WAV_DRIFT_BUG.md
@@ -1,7 +1,8 @@
# Audio WAV Drift Bug Investigation
**Date:** 2026-02-15
-**Status:** ROOT CAUSE IDENTIFIED
+**Status:** ACCEPTABLE (to be continued)
+**Current State:** -150ms drift at beat 64b, no glitches
## Problem Statement
@@ -163,8 +164,18 @@ Eliminates cumulative truncation error.
1. ✅ Measure WAV sample positions directly (Python script)
2. ✅ Add render tracking debug output
3. ✅ Confirm over-rendering (366ms per 10s)
-4. ⏳ Implement fix
-5. ⏳ Verify corrected WAV alignment in viewer
+4. ✅ Implement partial fix (bypass ring buffer, direct render)
+5. ⚠️ Current result: -150ms drift at beat 64b (acceptable, needs further work)
+
+## Current Implementation (main.cc:286-308)
+
+**WAV dump now bypasses ring buffer entirely:**
+1. **Frame accumulator**: Calculates exact frames per update (no truncation)
+2. **Direct render**: Calls `synth_render()` directly with exact frame count
+3. **No ring buffer**: Eliminates buffer management complexity
+4. **Result**: No glitches, but -150ms drift remains
+
+**Remaining issue:** Drift persists despite direct rendering. Likely related to tempo scaling or audio engine state management. Acceptable for now.
## Notes
diff --git a/src/app/main.cc b/src/app/main.cc
index 537da74..5648b90 100644
--- a/src/app/main.cc
+++ b/src/app/main.cc
@@ -207,7 +207,10 @@ int main(int argc, char** argv) {
#endif /* !defined(STRIP_ALL) */
// Pre-fill ring buffer to target lookahead (prevents startup delay)
- fill_audio_buffer(audio_get_required_prefill_time(), 0.0);
+ // Skip pre-fill in WAV dump mode (direct render, no ring buffer)
+ if (!dump_wav) {
+ fill_audio_buffer(audio_get_required_prefill_time(), 0.0);
+ }
audio_start();
g_last_audio_time = audio_get_playback_time(); // Initialize after start
@@ -268,37 +271,41 @@ int main(int argc, char** argv) {
printf("Running WAV dump simulation (%.1fs - %.1fs)...\n", start_time,
end_time);
- // Seek to start time if needed
+ // Seek to start time if needed (advance state without rendering)
if (start_time > 0.0f) {
const double step = 1.0 / 60.0;
for (double t = 0.0; t < start_time; t += step) {
- fill_audio_buffer(step, t);
- audio_render_silent((float)step);
+ g_audio_engine.update(g_music_time, (float)step * g_tempo_scale);
+ g_music_time += (float)step * g_tempo_scale;
}
printf("Seeked to %.1fs\n", start_time);
}
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
+ const int sample_rate = 32000;
- AudioRingBuffer* ring_buffer = audio_get_ring_buffer();
- std::vector<float> chunk_buffer(samples_per_update);
+ std::vector<float> chunk_buffer(2048); // Max samples for one update
double physical_time = start_time;
+ double frame_accumulator = 0.0;
while (physical_time < end_time) {
- // Update music time and tracker (using tempo logic from
- // fill_audio_buffer)
- fill_audio_buffer(update_dt, physical_time);
+ // Calculate exact frames for this update
+ frame_accumulator += sample_rate * update_dt;
+ const int frames_this_update = (int)frame_accumulator;
+ frame_accumulator -= frames_this_update;
+ const int samples_this_update = frames_this_update * 2;
- // Read rendered audio from ring buffer
- if (ring_buffer != nullptr) {
- ring_buffer->read(chunk_buffer.data(), samples_per_update);
- }
+ // Update tracker/audio state
+ g_audio_engine.update(g_music_time, update_dt * g_tempo_scale);
- // Write to WAV file
- wav_backend.write_audio(chunk_buffer.data(), samples_per_update);
+ // Render directly to buffer (bypass ring buffer)
+ if (frames_this_update > 0) {
+ synth_render(chunk_buffer.data(), frames_this_update);
+ wav_backend.write_audio(chunk_buffer.data(), samples_this_update);
+ }
+ // Advance music time
+ g_music_time += update_dt * g_tempo_scale;
physical_time += update_dt;
// Progress indicator every second
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
index f5bc4ab..ba76a28 100644
--- a/src/audio/audio.cc
+++ b/src/audio/audio.cc
@@ -108,10 +108,6 @@ void audio_render_ahead(float music_time, float dt, float target_fill) {
if (chunk_frames <= 0)
return;
- static int64_t g_total_render_calls = 0;
- static int64_t g_total_frames_rendered = 0;
- const int64_t frames_before = g_ring_buffer.get_total_written() / RING_BUFFER_CHANNELS;
-
// Keep rendering small chunks until buffer is full enough
while (true) {
// First, try to flush any pending samples from previous partial writes
@@ -228,19 +224,6 @@ void audio_render_ahead(float music_time, float dt, float target_fill) {
}
}
}
-
- // DEBUG: Track actual frames rendered vs expected
- const int64_t frames_after = g_ring_buffer.get_total_written() / RING_BUFFER_CHANNELS;
- const int64_t actual_rendered = frames_after - frames_before;
- g_total_render_calls++;
- g_total_frames_rendered += actual_rendered;
-
- if (g_total_render_calls % 600 == 0) { // Every 10 seconds at 60fps
- const float expected_frames = g_total_render_calls * (float)(chunk_frames);
- const float drift_ms = (expected_frames - g_total_frames_rendered) / RING_BUFFER_SAMPLE_RATE * 1000.0f;
- printf("[RENDER_DRIFT] calls=%lld expect=%.1f actual=%lld drift=%.2fms\n",
- g_total_render_calls, expected_frames, g_total_frames_rendered, drift_ms);
- }
}
float audio_get_playback_time() {
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 00c31e9..37f0683 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -324,14 +324,6 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
}
}
- // DEBUG: Track kick/snare timing for drift investigation
- if (event.sample_id == 0 || event.sample_id == 1) { // Assuming kick=0, snare=1
- const char* name = (event.sample_id == 0) ? "KICK " : "SNARE";
- const float delta_ms = (event_music_time - music_time_sec) * 1000.0f;
- printf("[DRIFT] %s: music=%.4f expect=%.4f delta=%.2fms offset=%d\n",
- name, music_time_sec, event_music_time, delta_ms, sample_offset);
- }
-
trigger_note_event(event, sample_offset, volume_mult);
active.next_event_idx++;
}
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
index bc7f2a0..9768343 100644
--- a/tools/timeline_editor/index.html
+++ b/tools/timeline_editor/index.html
@@ -45,7 +45,7 @@
.timeline { position: relative; min-height: 100%; }
.sticky-header { position: sticky; top: 0; background: var(--bg-medium); z-index: 100; padding: 20px 20px 10px 20px; border-bottom: 2px solid var(--bg-light); flex-shrink: 0; }
- .waveform-container { position: relative; height: 80px; overflow: hidden; background: rgba(0, 0, 0, 0.3); border-radius: var(--radius); cursor: crosshair; }
+ .waveform-container { position: relative; height: 80px; overflow: hidden; background: rgba(0, 0, 0, 0.5); border-radius: var(--radius); cursor: crosshair; }
#cpuLoadCanvas { position: absolute; left: 0; bottom: 0; height: 10px; display: block; z-index: 1; }
#waveformCanvas { position: absolute; left: 0; top: 0; height: 80px; display: block; z-index: 2; }
.waveform-cursor { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(78, 201, 176, 0.6); pointer-events: none; z-index: 3; display: none; }
@@ -56,7 +56,7 @@
.time-markers { position: relative; height: 30px; margin-top: var(--gap); border-bottom: 1px solid var(--bg-light); }
.time-marker { position: absolute; top: 0; font-size: 12px; color: var(--text-muted); }
.time-marker::before { content: ''; position: absolute; left: 0; top: 20px; width: 1px; height: 10px; background: var(--bg-light); }
- .time-marker::after { content: ''; position: absolute; left: 0; top: 30px; width: 1px; height: 10000px; background: rgba(60, 60, 60, 0.2); pointer-events: none; }
+ .time-marker::after { content: ''; position: absolute; left: 0; top: 30px; width: 1px; height: 10000px; background: rgba(100, 100, 60, 0.9); pointer-events: none; }
.sequence { position: absolute; background: #264f78; border: 2px solid var(--accent-blue); border-radius: var(--radius); padding: 8px; cursor: move; min-height: 40px; transition: box-shadow 0.2s; }
.sequence:hover { box-shadow: 0 0 10px rgba(14, 99, 156, 0.5); }
diff --git a/tools/timeline_editor/timeline-playback.js b/tools/timeline_editor/timeline-playback.js
index 8c84877..a1c50ab 100644
--- a/tools/timeline_editor/timeline-playback.js
+++ b/tools/timeline_editor/timeline-playback.js
@@ -168,7 +168,7 @@ export class PlaybackController {
ctx.stroke();
// Beat markers
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.50)';
ctx.lineWidth = 1;
for (let beat = 0; beat <= maxTimeBeats; beat++) {
const x = beat * this.state.pixelsPerBeat;