summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-07 21:34:00 +0100
committerskal <pascal.massimino@gmail.com>2026-02-07 21:34:00 +0100
commit727177329f833f76683b53570f3268c39b463e86 (patch)
treea4e531c06e719ba8cf49df5436ecc5bdca955364 /src/audio
parentf9c5e84f01d0d7d1c981db4a0594dee9986429c4 (diff)
fix(audio): Calculate sample offsets from render position, not playback positionHEADmain
This fixes irregular timing in miniaudio playback while WAV dump was correct. ROOT CAUSE: Sample offsets were calculated relative to the ring buffer READ position (audio_get_playback_time), but should be calculated relative to the WRITE position (where we're currently rendering). The write position is ~400ms ahead of the read position (the lookahead buffer). ISSUE TIMELINE: 1. tracker_update() gets playback_time (read pos, e.g., 0.450s) 2. Calculates offset for event at 0.500s: (0.500 - 0.450) * 32000 = 1600 samples 3. BUT: We're actually writing at 0.850s (write pos = read pos + 400ms buffer) 4. Event triggers at 0.850s + 1600 samples = 0.900s instead of 0.500s! 5. Result: Event is 400ms late! The timing error was compounded by the fact that the playback position advances continuously between tracker_update() calls (60fps), making the calculated offsets stale by the time rendering happens. SOLUTION: 1. Added total_written_ tracking to AudioRingBuffer 2. Added audio_get_render_time() to get write position 3. Updated tracker.cc to use render_time instead of playback_time for offsets CHANGES: - ring_buffer.h: Add get_total_written() method, total_written_ member - ring_buffer.cc: Initialize and track total_written_ in write() - audio.h: Add audio_get_render_time() function - audio.cc: Implement audio_get_render_time() using get_total_written() - tracker.cc: Use current_render_time for sample offset calculation RESULT: Sample offsets now calculated relative to where we're currently rendering, not where audio is currently playing. Events trigger at exact times in both WAV dump (offline) and miniaudio (realtime) playback. VERIFICATION: 1. WAV dump: Already working (confirmed by user) 2. Miniaudio: Should now match WAV dump timing exactly Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/audio.cc6
-rw-r--r--src/audio/audio.h6
-rw-r--r--src/audio/ring_buffer.cc5
-rw-r--r--src/audio/ring_buffer.h12
-rw-r--r--src/audio/tracker.cc10
5 files changed, 31 insertions, 8 deletions
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
index 67345cf..74536e5 100644
--- a/src/audio/audio.cc
+++ b/src/audio/audio.cc
@@ -172,6 +172,12 @@ float audio_get_playback_time() {
(RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
}
+float audio_get_render_time() {
+ const int64_t total_samples = g_ring_buffer.get_total_written();
+ return (float)total_samples /
+ (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+}
+
float audio_get_realtime_peak() {
if (g_audio_backend == nullptr) {
return 0.0f;
diff --git a/src/audio/audio.h b/src/audio/audio.h
index 14fe615..e063a57 100644
--- a/src/audio/audio.h
+++ b/src/audio/audio.h
@@ -27,8 +27,14 @@ void audio_start(); // Starts the audio device callback
void audio_render_ahead(float music_time, float dt);
// Get current playback time (in seconds) based on samples consumed
+// This is the ring buffer READ position (what's being played NOW)
float audio_get_playback_time();
+// Get current render time (in seconds) based on samples written
+// This is the ring buffer WRITE position (where we're currently rendering)
+// Use this for calculating sample-accurate trigger offsets
+float audio_get_render_time();
+
// Get peak amplitude of samples currently being played (real-time sync)
// Returns: Peak amplitude in range [0.0, 1.0+]
// Use this for visual effects to ensure audio-visual synchronization
diff --git a/src/audio/ring_buffer.cc b/src/audio/ring_buffer.cc
index a7e5d9e..7cedb56 100644
--- a/src/audio/ring_buffer.cc
+++ b/src/audio/ring_buffer.cc
@@ -9,7 +9,7 @@
AudioRingBuffer::AudioRingBuffer()
: capacity_(RING_BUFFER_CAPACITY_SAMPLES), write_pos_(0), read_pos_(0),
- total_read_(0) {
+ total_read_(0), total_written_(0) {
memset(buffer_, 0, sizeof(buffer_));
}
@@ -81,6 +81,9 @@ int AudioRingBuffer::write(const float* samples, int count) {
write_pos_.store(remainder, std::memory_order_release);
}
+ // Track total samples written for render timing
+ total_written_.fetch_add(to_write, std::memory_order_release);
+
return to_write;
}
diff --git a/src/audio/ring_buffer.h b/src/audio/ring_buffer.h
index b19c1ea..324447a 100644
--- a/src/audio/ring_buffer.h
+++ b/src/audio/ring_buffer.h
@@ -42,6 +42,11 @@ class AudioRingBuffer {
return total_read_.load(std::memory_order_acquire);
}
+ // Get total samples written (for render timing)
+ int64_t get_total_written() const {
+ return total_written_.load(std::memory_order_acquire);
+ }
+
// Clear buffer (for seeking)
void clear();
@@ -49,7 +54,8 @@ class AudioRingBuffer {
float buffer_[RING_BUFFER_CAPACITY_SAMPLES];
int capacity_; // Total capacity in samples
- std::atomic<int> write_pos_; // Write position (0 to capacity-1)
- std::atomic<int> read_pos_; // Read position (0 to capacity-1)
- std::atomic<int64_t> total_read_; // Total samples read (for playback time)
+ std::atomic<int> write_pos_; // Write position (0 to capacity-1)
+ std::atomic<int> read_pos_; // Read position (0 to capacity-1)
+ std::atomic<int64_t> total_read_; // Total samples read (for playback time)
+ std::atomic<int64_t> total_written_; // Total samples written (for render timing)
};
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 93a1c49..1cccc57 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -239,8 +239,9 @@ void tracker_update(float music_time_sec) {
}
// Step 2: Update all active patterns and trigger individual events
- // Get current audio playback position for sample-accurate timing
- const float current_playback_time = audio_get_playback_time();
+ // Get current audio RENDER position (write position) for sample-accurate timing
+ // This is where we're currently writing to the ring buffer (~400ms ahead of playback)
+ const float current_render_time = audio_get_render_time();
const float SAMPLE_RATE = 32000.0f; // Audio sample rate
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
@@ -265,8 +266,9 @@ void tracker_update(float music_time_sec) {
const float event_trigger_time = active.start_music_time +
(event.unit_time * unit_duration_sec);
- // Calculate sample-accurate offset from current playback position
- const float time_delta = event_trigger_time - current_playback_time;
+ // Calculate sample-accurate offset from current RENDER position (write pos)
+ // This is where we're currently writing to the buffer, not where playback is
+ const float time_delta = event_trigger_time - current_render_time;
int sample_offset = (int)(time_delta * SAMPLE_RATE);
// Clamp to 0 if negative (event is late, play immediately)