From d94e8ca1df4f6cf366afd33be43a7eed8d766560 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 4 Feb 2026 16:23:29 +0100 Subject: fix(audio): Render audio in small chunks to fix timing gaps Fixed issue where drum patterns had silence gaps between cycles. The problem was that audio_render_ahead was rendering audio in large chunks (up to 200ms), causing the synth internal time to become desynchronized from tracker events. Problem: - audio_render_ahead checked buffer fullness, then rendered large chunk - First call: buffer empty, render 200ms, synth advances by 200ms - Next 12 calls: buffer > 100ms, do not render, synth state frozen - Call 13: buffer < 100ms, render more, but tracker triggered events in between - Events triggered between render calls ended up at wrong synth time position - Result: Silence gaps between patterns Solution: - Changed audio_render_ahead to render in small incremental chunks - Chunk size: one frame worth of audio (~16.6ms @ 60fps) - Loop until buffer reaches target lookahead (200ms) - Synth now advances gradually, staying synchronized with tracker Result: Synth time stays synchronized with tracker event timing, no gaps. Testing: All 17 tests pass (100%) Co-Authored-By: Claude Sonnet 4.5 --- src/audio/audio.cc | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/audio/audio.cc b/src/audio/audio.cc index 7c0f490..779cb6c 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -73,39 +73,46 @@ void audio_start() { void audio_render_ahead(float music_time, float dt) { // Calculate how much audio is currently buffered - const int buffered_samples = g_ring_buffer.available_read(); - const float buffered_time = + int buffered_samples = g_ring_buffer.available_read(); + float buffered_time = (float)buffered_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); // Target: maintain look-ahead buffer const float target_lookahead = (float)RING_BUFFER_LOOKAHEAD_MS / 1000.0f; - // Only render if we're below target - if (buffered_time < target_lookahead * 0.5f) { // Refill when half empty - // Calculate how many frames to render - const float time_to_render = target_lookahead - buffered_time; - const int frames_to_render = - (int)(time_to_render * RING_BUFFER_SAMPLE_RATE); + // Render in small chunks to keep synth time synchronized with tracker + // Chunk size: one frame's worth of audio (~16.6ms @ 60fps) + const int chunk_frames = (int)(dt * RING_BUFFER_SAMPLE_RATE); + const int chunk_samples = chunk_frames * RING_BUFFER_CHANNELS; - if (frames_to_render > 0) { - // Allocate temporary buffer (stereo) - const int samples_to_render = frames_to_render * RING_BUFFER_CHANNELS; - float* temp_buffer = new float[samples_to_render]; + // Keep rendering small chunks until buffer is full enough + while (buffered_time < target_lookahead) { + const int frames_to_render = chunk_frames; + if (frames_to_render <= 0) break; - // Render audio from synth - synth_render(temp_buffer, frames_to_render); + // Allocate temporary buffer (stereo) + const int samples_to_render = frames_to_render * RING_BUFFER_CHANNELS; + float* temp_buffer = new float[samples_to_render]; - // Write to ring buffer - const int written = g_ring_buffer.write(temp_buffer, samples_to_render); + // Render audio from synth (advances synth state incrementally) + synth_render(temp_buffer, frames_to_render); - // Notify backend of frames rendered (for testing/tracking) - if (g_audio_backend != nullptr) { - g_audio_backend->on_frames_rendered(written / RING_BUFFER_CHANNELS); - } + // Write to ring buffer + const int written = g_ring_buffer.write(temp_buffer, samples_to_render); - delete[] temp_buffer; + // Notify backend of frames rendered (for testing/tracking) + if (g_audio_backend != nullptr) { + g_audio_backend->on_frames_rendered(written / RING_BUFFER_CHANNELS); } + + delete[] temp_buffer; + + // Update buffered time for next iteration + buffered_time += (float)written / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); + + // Safety: avoid infinite loop if buffer is full + if (written < samples_to_render) break; } } -- cgit v1.2.3