summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-07 14:00:23 +0100
committerskal <pascal.massimino@gmail.com>2026-02-07 14:00:23 +0100
commita6a7bf0440dbabdc6c994c0fb21a8ac31c27be07 (patch)
tree26663d3d65b110fca618d6fa33c83f7a8d1e362a /src/audio
parentda1d4e10731789191d8a23e60c3dd35217e6bdb0 (diff)
feat(audio): Add SilentBackend, fix peak measurement, reorganize backends
## Critical Fixes **Peak Measurement Timing:** - Fixed 400ms audio-visual desync by measuring peak at playback time - Added get_realtime_peak() to AudioBackend interface - Implemented real-time measurement in MiniaudioBackend audio callback - Updated main.cc and test_demo.cc to use audio_get_realtime_peak() **Peak Decay Rate:** - Fixed slow decay (0.95 → 0.7 per callback) - Old: 5.76 seconds to fade to 10% (constant flashing in test_demo) - New: 1.15 seconds to fade to 10% (proper visual sync) ## New Features **SilentBackend:** - Test-only backend for testing audio.cc without hardware - Controllable peak for testing edge cases - Tracks frames rendered and voice triggers - Added 7 comprehensive tests covering: - Lifecycle (init/start/shutdown) - Peak control and tracking - Playback time and buffer management - Integration with AudioEngine ## Refactoring **Backend Organization:** - Created src/audio/backend/ directory - Moved all backend implementations to subdirectory - Updated include paths and CMakeLists.txt - Cleaner codebase structure **Code Cleanup:** - Removed unused register_spec_asset() function - Added deprecation note to synth_get_output_peak() ## Testing - All 28 tests passing (100%) - New test: test_silent_backend - Improved audio.cc test coverage significantly ## Documentation - Created PEAK_FIX_SUMMARY.md with technical details - Created TASKS_SUMMARY.md with complete task report Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/audio.cc26
-rw-r--r--src/audio/audio.h8
-rw-r--r--src/audio/audio_backend.h5
-rw-r--r--src/audio/backend/jittered_audio_backend.cc (renamed from src/audio/jittered_audio_backend.cc)10
-rw-r--r--src/audio/backend/jittered_audio_backend.h (renamed from src/audio/jittered_audio_backend.h)3
-rw-r--r--src/audio/backend/miniaudio_backend.cc (renamed from src/audio/miniaudio_backend.cc)29
-rw-r--r--src/audio/backend/miniaudio_backend.h (renamed from src/audio/miniaudio_backend.h)7
-rw-r--r--src/audio/backend/mock_audio_backend.cc (renamed from src/audio/mock_audio_backend.cc)6
-rw-r--r--src/audio/backend/mock_audio_backend.h (renamed from src/audio/mock_audio_backend.h)3
-rw-r--r--src/audio/backend/silent_backend.cc50
-rw-r--r--src/audio/backend/silent_backend.h52
-rw-r--r--src/audio/backend/wav_dump_backend.cc (renamed from src/audio/wav_dump_backend.cc)6
-rw-r--r--src/audio/backend/wav_dump_backend.h (renamed from src/audio/wav_dump_backend.h)3
-rw-r--r--src/audio/synth.h4
14 files changed, 184 insertions, 28 deletions
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
index b00d416..67345cf 100644
--- a/src/audio/audio.cc
+++ b/src/audio/audio.cc
@@ -4,7 +4,7 @@
#include "audio.h"
#include "audio_backend.h"
-#include "miniaudio_backend.h"
+#include "backend/miniaudio_backend.h"
#include "ring_buffer.h"
#include "synth.h"
#include "util/asset_manager.h"
@@ -40,23 +40,6 @@ AudioBackend* audio_get_backend() {
}
#endif /* !defined(STRIP_ALL) */
-int register_spec_asset(AssetId id) {
- size_t size;
- const uint8_t* data = GetAsset(id, &size);
- if (!data || size < sizeof(SpecHeader))
- return -1;
-
- const SpecHeader* header = (const SpecHeader*)data;
- const float* spectral_data = (const float*)(data + sizeof(SpecHeader));
-
- Spectrogram spec;
- spec.spectral_data_a = spectral_data;
- spec.spectral_data_b = spectral_data; // No double-buffer for static assets
- spec.num_frames = header->num_frames;
-
- return synth_register_spectrogram(&spec);
-}
-
void audio_init() {
// Note: synth_init() must be called separately before using audio system.
// In production code, use AudioEngine::init() which manages initialization
@@ -189,6 +172,13 @@ float audio_get_playback_time() {
(RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
}
+float audio_get_realtime_peak() {
+ if (g_audio_backend == nullptr) {
+ return 0.0f;
+ }
+ return g_audio_backend->get_realtime_peak();
+}
+
// Expose ring buffer for backends
AudioRingBuffer* audio_get_ring_buffer() {
return &g_ring_buffer;
diff --git a/src/audio/audio.h b/src/audio/audio.h
index aaa5d45..14fe615 100644
--- a/src/audio/audio.h
+++ b/src/audio/audio.h
@@ -29,6 +29,12 @@ void audio_render_ahead(float music_time, float dt);
// Get current playback time (in seconds) based on samples consumed
float audio_get_playback_time();
+// Get peak amplitude of samples currently being played (real-time sync)
+// Returns: Peak amplitude in range [0.0, 1.0+]
+// Use this for visual effects to ensure audio-visual synchronization
+// Note: Measured at actual playback time, not pre-buffer time (~400ms ahead)
+float audio_get_realtime_peak();
+
#if !defined(STRIP_ALL)
void audio_render_silent(float duration_sec); // Fast-forwards audio state
// Backend injection for testing
@@ -37,5 +43,3 @@ AudioBackend* audio_get_backend();
#endif /* !defined(STRIP_ALL) */
void audio_update();
void audio_shutdown();
-
-int register_spec_asset(AssetId id);
diff --git a/src/audio/audio_backend.h b/src/audio/audio_backend.h
index 940e2b2..d9c4690 100644
--- a/src/audio/audio_backend.h
+++ b/src/audio/audio_backend.h
@@ -20,6 +20,11 @@ class AudioBackend {
// Clean up backend resources
virtual void shutdown() = 0;
+ // Get peak amplitude of samples currently being played (real-time sync)
+ // Returns: Peak amplitude in range [0.0, 1.0+]
+ // Note: This should measure peak at actual playback time, not pre-buffer time
+ virtual float get_realtime_peak() = 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/jittered_audio_backend.cc b/src/audio/backend/jittered_audio_backend.cc
index 8742aba..0c1c4a6 100644
--- a/src/audio/jittered_audio_backend.cc
+++ b/src/audio/backend/jittered_audio_backend.cc
@@ -4,8 +4,8 @@
#if !defined(STRIP_ALL)
#include "jittered_audio_backend.h"
-#include "audio.h"
-#include "ring_buffer.h"
+#include "../audio.h"
+#include "../ring_buffer.h"
#include <chrono>
#include <cstdlib>
#include <cstring>
@@ -109,4 +109,10 @@ void JitteredAudioBackend::audio_thread_loop() {
}
}
+float JitteredAudioBackend::get_realtime_peak() {
+ // Jittered backend: No real-time playback, return 0
+ // This backend simulates timing jitter but doesn't play audio
+ return 0.0f;
+}
+
#endif /* !defined(STRIP_ALL) */
diff --git a/src/audio/jittered_audio_backend.h b/src/audio/backend/jittered_audio_backend.h
index c246c48..8bda4c7 100644
--- a/src/audio/jittered_audio_backend.h
+++ b/src/audio/backend/jittered_audio_backend.h
@@ -6,7 +6,7 @@
#if !defined(STRIP_ALL)
-#include "audio_backend.h"
+#include "../audio_backend.h"
#include <atomic>
#include <thread>
@@ -19,6 +19,7 @@ class JitteredAudioBackend : public AudioBackend {
void init() override;
void start() override;
void shutdown() override;
+ float get_realtime_peak() override;
// Control simulation
void set_jitter_amount(float jitter_ms); // Random jitter in ms (default: 5ms)
diff --git a/src/audio/miniaudio_backend.cc b/src/audio/backend/miniaudio_backend.cc
index 0e6fce5..da8d558 100644
--- a/src/audio/miniaudio_backend.cc
+++ b/src/audio/backend/miniaudio_backend.cc
@@ -3,10 +3,15 @@
// Moved from audio.cc to enable backend abstraction for testing.
#include "miniaudio_backend.h"
-#include "audio.h"
-#include "ring_buffer.h"
+#include "../audio.h"
+#include "../ring_buffer.h"
#include "util/debug.h"
#include "util/fatal_error.h"
+#include <cmath>
+
+// Real-time peak measured at actual playback time
+// Updated in audio_callback when samples are read from ring buffer
+volatile float MiniaudioBackend::realtime_peak_ = 0.0f;
// Static callback for miniaudio (C API requirement)
void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput,
@@ -144,6 +149,22 @@ void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput,
samples_to_read - actually_read);
}
#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.7 gives ~1 second decay time for visual sync
+ // (At 128ms callbacks: 0.7^7.8 ≈ 0.1 after ~1 second)
+ if (frame_peak > realtime_peak_) {
+ realtime_peak_ = frame_peak; // Attack: instant
+ } else {
+ realtime_peak_ *= 0.7f; // Decay: fast (30% per callback)
+ }
}
#if defined(DEBUG_LOG_AUDIO)
@@ -259,3 +280,7 @@ void MiniaudioBackend::shutdown() {
ma_device_uninit(&device_);
initialized_ = false;
}
+
+float MiniaudioBackend::get_realtime_peak() {
+ return realtime_peak_;
+}
diff --git a/src/audio/miniaudio_backend.h b/src/audio/backend/miniaudio_backend.h
index 82c7b76..eb9019c 100644
--- a/src/audio/miniaudio_backend.h
+++ b/src/audio/backend/miniaudio_backend.h
@@ -4,7 +4,7 @@
#pragma once
-#include "audio_backend.h"
+#include "../audio_backend.h"
#include "miniaudio.h"
// Production audio backend using miniaudio library
@@ -17,6 +17,7 @@ class MiniaudioBackend : public AudioBackend {
void init() override;
void start() override;
void shutdown() override;
+ float get_realtime_peak() override;
// Get the underlying miniaudio device (for internal use)
ma_device* get_device() {
@@ -27,6 +28,10 @@ class MiniaudioBackend : public AudioBackend {
ma_device device_;
bool initialized_;
+ // Real-time peak measured at actual playback time (not pre-buffer)
+ // Updated in audio_callback when samples are read from ring buffer
+ static volatile float realtime_peak_;
+
// 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/mock_audio_backend.cc b/src/audio/backend/mock_audio_backend.cc
index 33ed35a..068d8a3 100644
--- a/src/audio/mock_audio_backend.cc
+++ b/src/audio/backend/mock_audio_backend.cc
@@ -24,6 +24,12 @@ void MockAudioBackend::shutdown() {
// No-op for mock backend
}
+float MockAudioBackend::get_realtime_peak() {
+ // Mock backend: Return synthetic peak for testing
+ // Can be enhanced later to track actual synth output if needed
+ return 0.5f;
+}
+
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/mock_audio_backend.h b/src/audio/backend/mock_audio_backend.h
index a4ee36d..55e89c5 100644
--- a/src/audio/mock_audio_backend.h
+++ b/src/audio/backend/mock_audio_backend.h
@@ -6,7 +6,7 @@
#if !defined(STRIP_ALL)
-#include "audio_backend.h"
+#include "../audio_backend.h"
#include <vector>
// Event structure for recorded voice triggers
@@ -28,6 +28,7 @@ class MockAudioBackend : public AudioBackend {
void init() override;
void start() override;
void shutdown() override;
+ float get_realtime_peak() 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
new file mode 100644
index 0000000..637dd68
--- /dev/null
+++ b/src/audio/backend/silent_backend.cc
@@ -0,0 +1,50 @@
+// This file is part of the 64k demo project.
+// Implementation of silent backend for testing audio.cc.
+
+#include "silent_backend.h"
+
+#if !defined(STRIP_ALL)
+
+SilentBackend::SilentBackend()
+ : initialized_(false), started_(false), frames_rendered_(0),
+ voice_trigger_count_(0), test_peak_(0.0f) {
+}
+
+SilentBackend::~SilentBackend() {
+ shutdown();
+}
+
+void SilentBackend::init() {
+ initialized_ = true;
+}
+
+void SilentBackend::start() {
+ started_ = true;
+}
+
+void SilentBackend::shutdown() {
+ initialized_ = false;
+ started_ = false;
+}
+
+float SilentBackend::get_realtime_peak() {
+ // Return controllable test peak
+ return test_peak_;
+}
+
+void SilentBackend::on_voice_triggered(float timestamp, int spectrogram_id,
+ float volume, float pan) {
+ // Track voice triggers for testing
+ (void)timestamp;
+ (void)spectrogram_id;
+ (void)volume;
+ (void)pan;
+ voice_trigger_count_.fetch_add(1);
+}
+
+void SilentBackend::on_frames_rendered(int num_frames) {
+ // Track total frames rendered for testing
+ frames_rendered_.fetch_add(num_frames);
+}
+
+#endif /* !defined(STRIP_ALL) */
diff --git a/src/audio/backend/silent_backend.h b/src/audio/backend/silent_backend.h
new file mode 100644
index 0000000..f7da42d
--- /dev/null
+++ b/src/audio/backend/silent_backend.h
@@ -0,0 +1,52 @@
+// This file is part of the 64k demo project.
+// It implements a silent test backend for testing audio.cc without hardware.
+// Useful for achieving high test coverage and triggering edge cases.
+
+#pragma once
+
+#if !defined(STRIP_ALL)
+
+#include "../audio_backend.h"
+#include <atomic>
+
+// Silent backend for testing - no audio output, pure inspection
+// Allows testing audio.cc logic (buffer management, playback time tracking)
+// without requiring audio hardware or miniaudio
+class SilentBackend : public AudioBackend {
+ public:
+ SilentBackend();
+ ~SilentBackend() override;
+
+ // AudioBackend interface
+ void init() override;
+ void start() override;
+ void shutdown() override;
+ float get_realtime_peak() override;
+
+ // Test inspection interface
+ bool is_initialized() const { return initialized_; }
+ bool is_started() const { return started_; }
+ int get_frames_rendered() const { return frames_rendered_.load(); }
+ int get_voice_trigger_count() const { return voice_trigger_count_.load(); }
+
+ // Manual control for testing edge cases
+ void set_peak(float peak) { test_peak_ = peak; }
+ void reset_stats() {
+ frames_rendered_ = 0;
+ voice_trigger_count_ = 0;
+ }
+
+ // Event hooks (inherited from AudioBackend)
+ void on_voice_triggered(float timestamp, int spectrogram_id, float volume,
+ float pan) override;
+ void on_frames_rendered(int num_frames) override;
+
+ private:
+ bool initialized_;
+ bool started_;
+ std::atomic<int> frames_rendered_;
+ std::atomic<int> voice_trigger_count_;
+ float test_peak_; // Controllable peak for testing
+};
+
+#endif /* !defined(STRIP_ALL) */
diff --git a/src/audio/wav_dump_backend.cc b/src/audio/backend/wav_dump_backend.cc
index df82f90..1158fb2 100644
--- a/src/audio/wav_dump_backend.cc
+++ b/src/audio/backend/wav_dump_backend.cc
@@ -107,6 +107,12 @@ void WavDumpBackend::shutdown() {
is_active_ = false;
}
+float WavDumpBackend::get_realtime_peak() {
+ // WAV dump: No real-time playback, return 0
+ // Could optionally track peak of last written chunk if needed
+ return 0.0f;
+}
+
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/wav_dump_backend.h b/src/audio/backend/wav_dump_backend.h
index c3c5302..de445d6 100644
--- a/src/audio/wav_dump_backend.h
+++ b/src/audio/backend/wav_dump_backend.h
@@ -5,7 +5,7 @@
#if !defined(DEMO_AUDIO_WAV_DUMP_BACKEND_H)
#define DEMO_AUDIO_WAV_DUMP_BACKEND_H
-#include "audio_backend.h"
+#include "../audio_backend.h"
#include <stdio.h>
#include <vector>
@@ -22,6 +22,7 @@ class WavDumpBackend : public AudioBackend {
void init() override;
void start() override;
void shutdown() override;
+ float get_realtime_peak() override;
// Set output filename (call before init())
void set_output_file(const char* filename);
diff --git a/src/audio/synth.h b/src/audio/synth.h
index cb0d1df..ba96167 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -44,4 +44,8 @@ void synth_set_tempo_scale(
float tempo_scale); // Set playback speed (1.0 = normal)
int synth_get_active_voice_count();
+
+// Get peak amplitude of synthesized audio (measured at ring buffer write time)
+// NOTE: For audio-visual synchronization, use audio_get_realtime_peak() instead
+// This function measures peak ~400ms ahead of playback (at synth_render time)
float synth_get_output_peak();