diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-04 16:23:29 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-04 16:23:29 +0100 |
| commit | d94e8ca1df4f6cf366afd33be43a7eed8d766560 (patch) | |
| tree | bda3e4c605b91949e632107474b19ed46c5ff359 | |
| parent | 77eb218e7c33676da19a695b8307149a2f8ebc13 (diff) | |
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 <noreply@anthropic.com>
| -rw-r--r-- | src/audio/audio.cc | 49 |
1 files changed, 28 insertions, 21 deletions
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; } } |
