diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-15 23:56:43 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-15 23:56:43 +0100 |
| commit | 5c7feffd3749ce4b355d0db6334cf39ca94d8d82 (patch) | |
| tree | 7105aea12d0367208a37777cf53b348f35a66dad /src | |
| parent | e21127a3fc4797805d49ae2d95fc7ed6f94ac456 (diff) | |
perf(audio): smooth playback time and RMS-based peak at 60Hz
Interpolates audio playback time between callbacks using CLOCK_MONOTONIC
for smooth 60Hz updates instead of coarse 8-10Hz steps.
Replaces artificial peak decay with true RMS calculation over 50ms
window. Ring buffer computes RMS directly on internal buffer without
copies for efficiency.
All backends updated with get_callback_state() interface for time
interpolation. Tests passing (34/34).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/main.cc | 17 | ||||
| -rw-r--r-- | src/audio/audio.cc | 32 | ||||
| -rw-r--r-- | src/audio/audio.h | 1 | ||||
| -rw-r--r-- | src/audio/audio_backend.h | 6 | ||||
| -rw-r--r-- | src/audio/backend/jittered_audio_backend.cc | 6 | ||||
| -rw-r--r-- | src/audio/backend/jittered_audio_backend.h | 1 | ||||
| -rw-r--r-- | src/audio/backend/miniaudio_backend.cc | 34 | ||||
| -rw-r--r-- | src/audio/backend/miniaudio_backend.h | 5 | ||||
| -rw-r--r-- | src/audio/backend/mock_audio_backend.cc | 6 | ||||
| -rw-r--r-- | src/audio/backend/mock_audio_backend.h | 1 | ||||
| -rw-r--r-- | src/audio/backend/silent_backend.cc | 5 | ||||
| -rw-r--r-- | src/audio/backend/silent_backend.h | 1 | ||||
| -rw-r--r-- | src/audio/backend/wav_dump_backend.cc | 6 | ||||
| -rw-r--r-- | src/audio/backend/wav_dump_backend.h | 1 | ||||
| -rw-r--r-- | src/audio/ring_buffer.cc | 39 | ||||
| -rw-r--r-- | src/audio/ring_buffer.h | 5 |
16 files changed, 138 insertions, 28 deletions
diff --git a/src/app/main.cc b/src/app/main.cc index 3c80520..9bbbaaf 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -168,7 +168,7 @@ int main(int argc, char** argv) { #if !defined(STRIP_ALL) // Debug output when tempo changes significantly if (fabsf(g_tempo_scale - prev_tempo) > 0.05f) { - printf("[Tempo] t=%.2fs, tempo=%.3fx, music_time=%.3fs\n", + printf("[Tempo] t=%.3fs, tempo=%.3fx, music_time=%.3fs\n", (float)physical_time, g_tempo_scale, g_music_time); } #endif @@ -191,7 +191,7 @@ int main(int argc, char** argv) { #if !defined(STRIP_ALL) if (seek_time > 0.0) { - printf("Seeking to %.2f seconds...\n", seek_time); + printf("Seeking to %.3f seconds...\n", seek_time); // Simulate audio/game logic // We step at ~60hz @@ -213,7 +213,7 @@ int main(int argc, char** argv) { } audio_start(); - g_last_audio_time = audio_get_playback_time(); // Initialize after start + g_last_audio_time = 0.0f; // Initialize to zero (will use smooth interpolation) #if !defined(STRIP_ALL) // Hot-reload setup @@ -351,7 +351,7 @@ int main(int argc, char** argv) { // Graphics frame time - derived from platform's clock const double current_physical_time = platform_state.time + seek_time; const double graphics_frame_time = current_physical_time - last_frame_time; - // Audio playback time - master clock for audio events + // Audio playback time - smoothly interpolated at 60Hz const float current_audio_time = audio_get_playback_time(); // Delta time for audio processing, based on audio clock const float audio_dt = current_audio_time - g_last_audio_time; @@ -398,17 +398,18 @@ int main(int argc, char** argv) { // Use graphics time for the print interval to avoid excessive output if // audio clock is slow static float last_graphics_print_time = -1.0f; - if (current_physical_time - last_graphics_print_time >= + if (true || + current_physical_time - last_graphics_print_time >= 0.5f) { // Print every 0.5 seconds if (tempo_test_enabled) { printf( - "[GraphicsT=%.2f, AudioT=%.2f, MusicT=%.2f, Beat=%d, Phase=%.2f, " - "Peak=%.2f, Tempo=%.2fx]\n", + "[GraphicsT=%.3f, AudioT=%.3f, MusicT=%.3f, Beat=%d, Phase=%.3f, " + "Peak=%.3f, Tempo=%.3fx]\n", current_physical_time, current_audio_time, g_music_time, beat_number, beat_phase, visual_peak, g_tempo_scale); } else { printf( - "[GraphicsT=%.2f, AudioT=%.2f, Beat=%d, Phase=%.2f, Peak=%.2f]\n", + "[GraphicsT=%.3f, AudioT=%.3f, Beat=%d, Phase=%.3f, Peak=%.3f]\n", current_physical_time, current_audio_time, beat_number, beat_phase, visual_peak); } diff --git a/src/audio/audio.cc b/src/audio/audio.cc index a220fbb..780691a 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -12,6 +12,7 @@ #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" +#include <cmath> #include <stdio.h> // Global ring buffer for audio streaming @@ -228,6 +229,27 @@ void audio_render_ahead(float music_time, float dt, float target_fill) { } float audio_get_playback_time() { + // Smooth interpolation: use backend callback state if available + if (g_audio_backend != nullptr) { + double last_callback_time = 0.0; + int64_t last_callback_samples = 0; + g_audio_backend->get_callback_state(&last_callback_time, + &last_callback_samples); + + // If callback has fired, interpolate for smooth 60Hz updates + if (last_callback_time != 0.0) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + const double current_time = ts.tv_sec + ts.tv_nsec / 1e9; + const double elapsed = current_time - last_callback_time; + const float interpolated_samples = + (float)(elapsed * RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); + const float total_samples = (float)last_callback_samples + interpolated_samples; + return total_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); + } + } + + // Fallback: coarse ring buffer time (before first callback or no backend) const int64_t total_samples = g_ring_buffer.get_total_read(); return (float)total_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS); @@ -240,10 +262,12 @@ float audio_get_render_time() { } float audio_get_realtime_peak() { - if (g_audio_backend == nullptr) { - return 0.0f; - } - return g_audio_backend->get_realtime_peak(); + // Calculate RMS over recent time window (50ms) + const int window_ms = 50; + const int window_samples = + (window_ms * RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS) / 1000; + + return g_ring_buffer.get_past_rms(window_samples); } // Expose ring buffer for backends diff --git a/src/audio/audio.h b/src/audio/audio.h index beb994f..97a8c99 100644 --- a/src/audio/audio.h +++ b/src/audio/audio.h @@ -36,6 +36,7 @@ void audio_render_ahead(float music_time, float dt, float target_fill = -1.0f); // Get current playback time (in seconds) based on samples consumed // This is the ring buffer READ position (what's being played NOW) +// Smoothly interpolated between audio callbacks for 60Hz updates float audio_get_playback_time(); // Get current render time (in seconds) based on samples written diff --git a/src/audio/audio_backend.h b/src/audio/audio_backend.h index d9c4690..980ee6e 100644 --- a/src/audio/audio_backend.h +++ b/src/audio/audio_backend.h @@ -3,6 +3,7 @@ // Enables testing without hardware by abstracting audio output. #pragma once +#include <cstdint> // AudioBackend interface for audio output abstraction // Production uses MiniaudioBackend, tests use MockAudioBackend @@ -25,6 +26,11 @@ class AudioBackend { // Note: This should measure peak at actual playback time, not pre-buffer time virtual float get_realtime_peak() = 0; + // Get callback state for smooth time interpolation + // out_time: Timestamp of last audio callback (seconds) + // out_samples: Total samples read at last callback + virtual void get_callback_state(double* out_time, int64_t* out_samples) = 0; + #if !defined(STRIP_ALL) // Hook called when a voice is triggered (test-only) // timestamp: Time in seconds when voice was triggered diff --git a/src/audio/backend/jittered_audio_backend.cc b/src/audio/backend/jittered_audio_backend.cc index 0c1c4a6..973a474 100644 --- a/src/audio/backend/jittered_audio_backend.cc +++ b/src/audio/backend/jittered_audio_backend.cc @@ -115,4 +115,10 @@ float JitteredAudioBackend::get_realtime_peak() { return 0.0f; } +void JitteredAudioBackend::get_callback_state(double* out_time, + int64_t* out_samples) { + *out_time = 0.0; + *out_samples = 0; +} + #endif /* !defined(STRIP_ALL) */ diff --git a/src/audio/backend/jittered_audio_backend.h b/src/audio/backend/jittered_audio_backend.h index 8bda4c7..13feee0 100644 --- a/src/audio/backend/jittered_audio_backend.h +++ b/src/audio/backend/jittered_audio_backend.h @@ -20,6 +20,7 @@ class JitteredAudioBackend : public AudioBackend { void start() override; void shutdown() override; float get_realtime_peak() override; + void get_callback_state(double* out_time, int64_t* out_samples) override; // Control simulation void set_jitter_amount(float jitter_ms); // Random jitter in ms (default: 5ms) diff --git a/src/audio/backend/miniaudio_backend.cc b/src/audio/backend/miniaudio_backend.cc index ffa0852..26194c9 100644 --- a/src/audio/backend/miniaudio_backend.cc +++ b/src/audio/backend/miniaudio_backend.cc @@ -13,6 +13,10 @@ // Updated in audio_callback when samples are read from ring buffer volatile float MiniaudioBackend::realtime_peak_ = 0.0f; +// Smooth playback time interpolation +volatile double MiniaudioBackend::last_callback_time_ = 0.0; +volatile int64_t MiniaudioBackend::last_callback_samples_ = 0; + // Static callback for miniaudio (C API requirement) void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput, const void* pInput, @@ -140,6 +144,12 @@ void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput, const int actually_read = ring_buffer->read(fOutput, samples_to_read); + // Update smooth playback time tracking (absolute time, no epoch needed) + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + last_callback_time_ = ts.tv_sec + ts.tv_nsec / 1e9; + last_callback_samples_ = ring_buffer->get_total_read(); + #if defined(DEBUG_LOG_RING_BUFFER) if (actually_read < samples_to_read) { DEBUG_RING_BUFFER( @@ -150,22 +160,8 @@ void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput, } #endif /* defined(DEBUG_LOG_RING_BUFFER) */ - // Measure peak of samples being played RIGHT NOW (real-time sync) - // This ensures visual effects trigger at the same moment audio is heard - float frame_peak = 0.0f; - for (int i = 0; i < actually_read; ++i) { - frame_peak = fmaxf(frame_peak, fabsf(fOutput[i])); - } - - // Exponential averaging: instant attack, fast decay - // Decay rate of 0.5 gives ~500ms decay time for 120 BPM music - // (At 128ms callbacks: 0.5^3.9 ≈ 0.07 after ~500ms = 1 beat) - // TODO: Make decay rate configurable based on BPM from tracker/MainSequence - if (frame_peak > realtime_peak_) { - realtime_peak_ = frame_peak; // Attack: instant - } else { - realtime_peak_ *= 0.5f; // Decay: 50% per callback - } + // Peak calculation moved to audio_get_realtime_peak() for RMS-based measurement + // (uses ring buffer peek for accurate time-windowed RMS) } #if defined(DEBUG_LOG_AUDIO) @@ -285,3 +281,9 @@ void MiniaudioBackend::shutdown() { float MiniaudioBackend::get_realtime_peak() { return realtime_peak_; } + +void MiniaudioBackend::get_callback_state(double* out_time, + int64_t* out_samples) { + *out_time = last_callback_time_; + *out_samples = last_callback_samples_; +} diff --git a/src/audio/backend/miniaudio_backend.h b/src/audio/backend/miniaudio_backend.h index eb9019c..435496c 100644 --- a/src/audio/backend/miniaudio_backend.h +++ b/src/audio/backend/miniaudio_backend.h @@ -18,6 +18,7 @@ class MiniaudioBackend : public AudioBackend { void start() override; void shutdown() override; float get_realtime_peak() override; + void get_callback_state(double* out_time, int64_t* out_samples) override; // Get the underlying miniaudio device (for internal use) ma_device* get_device() { @@ -32,6 +33,10 @@ class MiniaudioBackend : public AudioBackend { // Updated in audio_callback when samples are read from ring buffer static volatile float realtime_peak_; + // Smooth playback time interpolation (updated in callback) + static volatile double last_callback_time_; // Absolute CLOCK_MONOTONIC time + static volatile int64_t last_callback_samples_; + // Static callback required by miniaudio C API static void audio_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); diff --git a/src/audio/backend/mock_audio_backend.cc b/src/audio/backend/mock_audio_backend.cc index 068d8a3..60be429 100644 --- a/src/audio/backend/mock_audio_backend.cc +++ b/src/audio/backend/mock_audio_backend.cc @@ -30,6 +30,12 @@ float MockAudioBackend::get_realtime_peak() { return 0.5f; } +void MockAudioBackend::get_callback_state(double* out_time, + int64_t* out_samples) { + *out_time = (double)current_time_sec_; + *out_samples = (int64_t)(current_time_sec_ * kSampleRate * 2); +} + void MockAudioBackend::on_voice_triggered(float timestamp, int spectrogram_id, float volume, float pan) { // Record the event with the timestamp provided by synth diff --git a/src/audio/backend/mock_audio_backend.h b/src/audio/backend/mock_audio_backend.h index 55e89c5..e998909 100644 --- a/src/audio/backend/mock_audio_backend.h +++ b/src/audio/backend/mock_audio_backend.h @@ -29,6 +29,7 @@ class MockAudioBackend : public AudioBackend { void start() override; void shutdown() override; float get_realtime_peak() override; + void get_callback_state(double* out_time, int64_t* out_samples) override; // Event recording hooks void on_voice_triggered(float timestamp, int spectrogram_id, float volume, diff --git a/src/audio/backend/silent_backend.cc b/src/audio/backend/silent_backend.cc index 6615eff..d6e1b19 100644 --- a/src/audio/backend/silent_backend.cc +++ b/src/audio/backend/silent_backend.cc @@ -32,6 +32,11 @@ float SilentBackend::get_realtime_peak() { return test_peak_; } +void SilentBackend::get_callback_state(double* out_time, int64_t* out_samples) { + *out_time = 0.0; + *out_samples = 0; +} + void SilentBackend::on_voice_triggered(float timestamp, int spectrogram_id, float volume, float pan) { // Track voice triggers for testing diff --git a/src/audio/backend/silent_backend.h b/src/audio/backend/silent_backend.h index 2d52858..c3942da 100644 --- a/src/audio/backend/silent_backend.h +++ b/src/audio/backend/silent_backend.h @@ -22,6 +22,7 @@ class SilentBackend : public AudioBackend { void start() override; void shutdown() override; float get_realtime_peak() override; + void get_callback_state(double* out_time, int64_t* out_samples) override; // Test inspection interface bool is_initialized() const { diff --git a/src/audio/backend/wav_dump_backend.cc b/src/audio/backend/wav_dump_backend.cc index 7427fa9..67f639f 100644 --- a/src/audio/backend/wav_dump_backend.cc +++ b/src/audio/backend/wav_dump_backend.cc @@ -114,6 +114,12 @@ float WavDumpBackend::get_realtime_peak() { return 0.0f; } +void WavDumpBackend::get_callback_state(double* out_time, + int64_t* out_samples) { + *out_time = 0.0; + *out_samples = 0; +} + void WavDumpBackend::write_wav_header(FILE* file, uint32_t num_samples) { // WAV file header structure // Reference: http://soundfile.sapp.org/doc/WaveFormat/ diff --git a/src/audio/backend/wav_dump_backend.h b/src/audio/backend/wav_dump_backend.h index de445d6..b59d406 100644 --- a/src/audio/backend/wav_dump_backend.h +++ b/src/audio/backend/wav_dump_backend.h @@ -23,6 +23,7 @@ class WavDumpBackend : public AudioBackend { void start() override; void shutdown() override; float get_realtime_peak() override; + void get_callback_state(double* out_time, int64_t* out_samples) override; // Set output filename (call before init()) void set_output_file(const char* filename); diff --git a/src/audio/ring_buffer.cc b/src/audio/ring_buffer.cc index 30566c9..54dd25b 100644 --- a/src/audio/ring_buffer.cc +++ b/src/audio/ring_buffer.cc @@ -5,6 +5,7 @@ #include "util/debug.h" #include "util/fatal_error.h" #include <algorithm> +#include <cmath> #include <cstring> AudioRingBuffer::AudioRingBuffer() @@ -178,3 +179,41 @@ void AudioRingBuffer::commit_write(int num_samples) { std::memory_order_release); total_written_.fetch_add(num_samples, std::memory_order_release); } + +float AudioRingBuffer::get_past_rms(int window_samples) const { + const int avail = available_read(); + const int to_read = std::min(window_samples, avail); + + if (to_read <= 0) { + return 0.0f; + } + + // Calculate start position (backward from read position) + int start = read_pos_.load(std::memory_order_acquire); + start = (start - to_read + capacity_) % capacity_; + + // Calculate RMS directly from ring buffer + float sum_squares = 0.0f; + const int space_to_end = capacity_ - start; + + if (to_read <= space_to_end) { + // One contiguous chunk + for (int i = 0; i < to_read; ++i) { + const float sample = buffer_[start + i]; + sum_squares += sample * sample; + } + } else { + // Two chunks (wrap around) + for (int i = 0; i < space_to_end; ++i) { + const float sample = buffer_[start + i]; + sum_squares += sample * sample; + } + const int remainder = to_read - space_to_end; + for (int i = 0; i < remainder; ++i) { + const float sample = buffer_[i]; + sum_squares += sample * sample; + } + } + + return sqrtf(sum_squares / (float)to_read); +} diff --git a/src/audio/ring_buffer.h b/src/audio/ring_buffer.h index 524cb29..1a21542 100644 --- a/src/audio/ring_buffer.h +++ b/src/audio/ring_buffer.h @@ -60,6 +60,11 @@ class AudioRingBuffer { // FATAL ERROR if num_samples exceeds region from get_write_region() void commit_write(int num_samples); + // Get RMS of recent samples without consuming + // window_samples: Number of samples to include in RMS calculation + // Returns: RMS value (root mean square) over the window + float get_past_rms(int window_samples) const; + private: float buffer_[RING_BUFFER_CAPACITY_SAMPLES]; int capacity_; // Total capacity in samples |
