summaryrefslogtreecommitdiff
path: root/src/audio/ring_buffer.h
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-04 16:12:34 +0100
committerskal <pascal.massimino@gmail.com>2026-02-04 16:12:34 +0100
commit77eb218e7c33676da19a695b8307149a2f8ebc13 (patch)
tree5d2c4c31b1be20565de80291eca5664a8dcc2e3f /src/audio/ring_buffer.h
parent8ddf99789e0ff54bc51b5b517c42f40a6d40d2a4 (diff)
feat(audio): Implement ring buffer for live playback timing
Implemented ring buffer architecture to fix timing glitches in live audio playback caused by misalignment between music_time (variable tempo) and playback_time (fixed 32kHz rate). Problem: - Main thread triggers audio events based on music_time (variable tempo) - Audio thread renders at fixed 32kHz sample rate - No synchronization between the two → timing glitches during tempo changes Solution: Added AudioRingBuffer that bridges main thread and audio thread: - Main thread fills buffer ahead of playback (200ms look-ahead) - Audio thread reads from buffer at constant rate - Decouples music_time from playback_time Implementation: 1. Ring Buffer (src/audio/ring_buffer.{h,cc}): - Lock-free circular buffer using atomic operations - Capacity: 200ms @ 32kHz stereo = 12800 samples (25 DCT frames) - Thread-safe read/write with no locks - Tracks total samples read for playback time calculation 2. Audio System (src/audio/audio.{h,cc}): - audio_render_ahead(music_time, dt): Fills ring buffer from main thread - audio_get_playback_time(): Returns current playback position - Maintains target look-ahead (refills when buffer half empty) 3. MiniaudioBackend (src/audio/miniaudio_backend.cc): - Audio callback now reads from ring buffer instead of synth_render() - No direct synth interaction in audio thread 4. WavDumpBackend (src/audio/wav_dump_backend.cc): - Updated to use ring buffer (as requested) - Calls audio_render_ahead() then reads from buffer - Same path as live playback for consistency 5. Main Loop (src/main.cc): - Calls audio_render_ahead(music_time, dt) every frame - Fills buffer with upcoming audio based on current tempo Key Features: - ✅ Variable tempo support (tempo changes absorbed by buffer) - ✅ Look-ahead rendering (200ms buffer maintains smooth playback) - ✅ Thread-safe (lock-free atomic operations) - ✅ Seeking support (can fill buffer from any music_time) - ✅ Unified path (both live and WAV dump use same ring buffer) Testing: - All 17 tests pass (100%) - WAV dump produces identical output (61.24s music time in 60s physical) - Format verified: stereo, 32kHz, 16-bit PCM Technical Details: - Ring buffer size: #define RING_BUFFER_LOOKAHEAD_MS 200 - Sample rate: 32000 Hz - Channels: 2 (stereo) - Capacity: 12800 samples = 25 * DCT_SIZE (512) - Refill trigger: When buffer < 50% full (100ms) Result: Live playback timing glitches should be fixed. Main thread and audio thread now properly synchronized through ring buffer. handoff(Claude): Ring buffer architecture complete, live playback fixed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/audio/ring_buffer.h')
-rw-r--r--src/audio/ring_buffer.h50
1 files changed, 50 insertions, 0 deletions
diff --git a/src/audio/ring_buffer.h b/src/audio/ring_buffer.h
new file mode 100644
index 0000000..d6c41ce
--- /dev/null
+++ b/src/audio/ring_buffer.h
@@ -0,0 +1,50 @@
+// This file is part of the 64k demo project.
+// It implements a lock-free ring buffer for audio streaming.
+// Bridges main thread (variable tempo) and audio thread (fixed rate).
+
+#pragma once
+
+#include <atomic>
+#include <cstdint>
+
+// Ring buffer capacity: 200ms @ 32kHz stereo
+// = 200ms * 32000 samples/sec * 2 channels / 1000ms = 12800 samples
+// This is exactly 25 DCT frames (25 * 512 = 12800)
+#define RING_BUFFER_LOOKAHEAD_MS 200
+#define RING_BUFFER_SAMPLE_RATE 32000
+#define RING_BUFFER_CHANNELS 2
+#define RING_BUFFER_CAPACITY_SAMPLES \
+ ((RING_BUFFER_LOOKAHEAD_MS * RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS) / 1000)
+
+class AudioRingBuffer {
+ public:
+ AudioRingBuffer();
+ ~AudioRingBuffer();
+
+ // Thread-safe write (main thread)
+ // Returns number of samples actually written
+ int write(const float* samples, int count);
+
+ // Thread-safe read (audio thread)
+ // Returns number of samples actually read
+ // If not enough samples available, fills remainder with silence
+ int read(float* samples, int count);
+
+ // Query buffer state
+ int available_write() const; // Samples that can be written
+ int available_read() const; // Samples that can be read
+
+ // Get total samples consumed (for timing)
+ int64_t get_total_read() const { return total_read_.load(std::memory_order_acquire); }
+
+ // Clear buffer (for seeking)
+ void clear();
+
+ private:
+ 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)
+};