From c4888bea71326f7a69e8214af0d9c2a62a60b887 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 4 Feb 2026 13:08:13 +0100 Subject: feat(audio): Implement audio backend abstraction (Task #51.1) Created interface-based audio backend system to enable testing without hardware. This is the foundation for robust tracker timing verification. Changes: - Created AudioBackend interface with init/start/shutdown methods - Added test-only hooks: on_voice_triggered() and on_frames_rendered() - Moved miniaudio implementation to MiniaudioBackend class - Refactored audio.cc to use backend abstraction with auto-fallback - Added time tracking to synth.cc (elapsed time from rendered frames) - Created test_audio_backend.cc to verify backend injection works - Fixed audio test linking to include util/procedural dependencies All test infrastructure guarded by #if !defined(STRIP_ALL) for zero size impact on final build. Production path unchanged, 100% backward compatible. All 13 tests pass. handoff(Claude): Task #51.1 complete, audio backend abstraction ready Co-Authored-By: Claude Sonnet 4.5 --- src/audio/audio.cc | 69 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 25 deletions(-) (limited to 'src/audio/audio.cc') diff --git a/src/audio/audio.cc b/src/audio/audio.cc index d08a2fa..6ee9782 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -1,16 +1,35 @@ // This file is part of the 64k demo project. // It manages the low-level audio device and high-level audio state. -// Implementation uses miniaudio for cross-platform support. +// Now uses backend abstraction for testability. #include "audio.h" +#include "audio_backend.h" +#include "miniaudio_backend.h" +#include "synth.h" #include "util/asset_manager.h" #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" -#include "synth.h" #include +// Global backend pointer for audio abstraction +static AudioBackend* g_audio_backend = nullptr; +static MiniaudioBackend g_default_backend; +static bool g_using_default_backend = false; + +#if !defined(STRIP_ALL) +// Allow tests to inject a custom backend +void audio_set_backend(AudioBackend* backend) { + g_audio_backend = backend; +} + +// Get current backend (for tests) +AudioBackend* audio_get_backend() { + return g_audio_backend; +} +#endif /* !defined(STRIP_ALL) */ + int register_spec_asset(AssetId id) { size_t size; const uint8_t* data = GetAsset(id, &size); @@ -28,36 +47,24 @@ int register_spec_asset(AssetId id) { return synth_register_spectrogram(&spec); } -static ma_device g_device; - -void audio_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, - ma_uint32 frameCount) { - (void)pInput; - float* fOutput = (float*)pOutput; - synth_render(fOutput, (int)frameCount); -} - void audio_init() { synth_init(); - ma_device_config config = ma_device_config_init(ma_device_type_playback); - config.playback.format = ma_format_f32; - config.playback.channels = 2; - config.sampleRate = 32000; - config.dataCallback = audio_data_callback; - - if (ma_device_init(NULL, &config, &g_device) != MA_SUCCESS) { - printf("Failed to open playback device.\n"); - return; + // Use default backend if none set + if (g_audio_backend == nullptr) { + g_audio_backend = &g_default_backend; + g_using_default_backend = true; } + + g_audio_backend->init(); } void audio_start() { - if (ma_device_start(&g_device) != MA_SUCCESS) { - printf("Failed to start playback device.\n"); - ma_device_uninit(&g_device); + if (g_audio_backend == nullptr) { + printf("Cannot start: audio not initialized.\n"); return; } + g_audio_backend->start(); } #if !defined(STRIP_ALL) @@ -72,6 +79,11 @@ void audio_render_silent(float duration_sec) { (total_frames > chunk_size) ? chunk_size : total_frames; synth_render(buffer, frames_to_render); total_frames -= frames_to_render; + + // Notify backend of frames rendered (for mock tracking) + if (g_audio_backend != nullptr) { + g_audio_backend->on_frames_rendered(frames_to_render); + } } } #endif /* !defined(STRIP_ALL) */ @@ -80,7 +92,14 @@ void audio_update() { } void audio_shutdown() { - ma_device_stop(&g_device); - ma_device_uninit(&g_device); + if (g_audio_backend != nullptr) { + g_audio_backend->shutdown(); + } synth_shutdown(); + + // Clear backend pointer if using default + if (g_using_default_backend) { + g_audio_backend = nullptr; + g_using_default_backend = false; + } } -- cgit v1.2.3