From 5c7feffd3749ce4b355d0db6334cf39ca94d8d82 Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 15 Feb 2026 23:56:43 +0100 Subject: 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 --- src/audio/backend/jittered_audio_backend.cc | 6 +++++ src/audio/backend/jittered_audio_backend.h | 1 + src/audio/backend/miniaudio_backend.cc | 34 +++++++++++++++-------------- src/audio/backend/miniaudio_backend.h | 5 +++++ src/audio/backend/mock_audio_backend.cc | 6 +++++ src/audio/backend/mock_audio_backend.h | 1 + src/audio/backend/silent_backend.cc | 5 +++++ src/audio/backend/silent_backend.h | 1 + src/audio/backend/wav_dump_backend.cc | 6 +++++ src/audio/backend/wav_dump_backend.h | 1 + 10 files changed, 50 insertions(+), 16 deletions(-) (limited to 'src/audio/backend') 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); -- cgit v1.2.3