summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-04 13:08:13 +0100
committerskal <pascal.massimino@gmail.com>2026-02-04 13:08:13 +0100
commitc4888bea71326f7a69e8214af0d9c2a62a60b887 (patch)
treedd1e71be51893414c590db8a7be578ddfd6232f0 /src/audio
parentf64f842bb0dabd89308e2378e56358bc8abdd653 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/audio.cc69
-rw-r--r--src/audio/audio.h8
-rw-r--r--src/audio/audio_backend.h44
-rw-r--r--src/audio/miniaudio_backend.cc70
-rw-r--r--src/audio/miniaudio_backend.h31
-rw-r--r--src/audio/synth.cc27
6 files changed, 223 insertions, 26 deletions
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 <stdio.h>
+// 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;
+ }
}
diff --git a/src/audio/audio.h b/src/audio/audio.h
index a1ddb44..52ad103 100644
--- a/src/audio/audio.h
+++ b/src/audio/audio.h
@@ -6,6 +6,9 @@
#include "generated/assets.h"
#include <cstdint>
+// Forward declaration for backend abstraction
+class AudioBackend;
+
struct SpecHeader {
char magic[4];
int32_t version;
@@ -17,7 +20,10 @@ void audio_init();
void audio_start(); // Starts the audio device callback
#if !defined(STRIP_ALL)
void audio_render_silent(float duration_sec); // Fast-forwards audio state
-#endif /* !defined(STRIP_ALL) */
+// Backend injection for testing
+void audio_set_backend(AudioBackend* backend);
+AudioBackend* audio_get_backend();
+#endif /* !defined(STRIP_ALL) */
void audio_update();
void audio_shutdown();
diff --git a/src/audio/audio_backend.h b/src/audio/audio_backend.h
new file mode 100644
index 0000000..4a2b7fb
--- /dev/null
+++ b/src/audio/audio_backend.h
@@ -0,0 +1,44 @@
+// This file is part of the 64k demo project.
+// It defines the interface for audio backend implementations.
+// Enables testing without hardware by abstracting audio output.
+
+#pragma once
+
+// AudioBackend interface for audio output abstraction
+// Production uses MiniaudioBackend, tests use MockAudioBackend
+class AudioBackend {
+ public:
+ virtual ~AudioBackend() {}
+
+ // Initialize backend resources
+ virtual void init() = 0;
+
+ // Start audio playback/recording
+ virtual void start() = 0;
+
+ // Clean up backend resources
+ virtual void shutdown() = 0;
+
+#if !defined(STRIP_ALL)
+ // Hook called when a voice is triggered (test-only)
+ // timestamp: Time in seconds when voice was triggered
+ // spectrogram_id: ID of the spectrogram being played
+ // volume: Voice volume (0.0 - 1.0)
+ // pan: Pan position (-1.0 left, 0.0 center, 1.0 right)
+ virtual void on_voice_triggered(float timestamp, int spectrogram_id,
+ float volume, float pan) {
+ // Default implementation does nothing (production path)
+ (void)timestamp;
+ (void)spectrogram_id;
+ (void)volume;
+ (void)pan;
+ }
+
+ // Hook called after rendering audio frames (test-only)
+ // num_frames: Number of frames rendered
+ virtual void on_frames_rendered(int num_frames) {
+ // Default implementation does nothing (production path)
+ (void)num_frames;
+ }
+#endif /* !defined(STRIP_ALL) */
+};
diff --git a/src/audio/miniaudio_backend.cc b/src/audio/miniaudio_backend.cc
new file mode 100644
index 0000000..d2563c5
--- /dev/null
+++ b/src/audio/miniaudio_backend.cc
@@ -0,0 +1,70 @@
+// This file is part of the 64k demo project.
+// It implements the production audio backend using miniaudio.
+// Moved from audio.cc to enable backend abstraction for testing.
+
+#include "miniaudio_backend.h"
+#include "synth.h"
+#include <stdio.h>
+
+// Static callback for miniaudio (C API requirement)
+void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput,
+ const void* pInput,
+ ma_uint32 frameCount) {
+ (void)pDevice;
+ (void)pInput;
+ float* fOutput = (float*)pOutput;
+ synth_render(fOutput, (int)frameCount);
+}
+
+MiniaudioBackend::MiniaudioBackend() : initialized_(false) {
+}
+
+MiniaudioBackend::~MiniaudioBackend() {
+ if (initialized_) {
+ shutdown();
+ }
+}
+
+void MiniaudioBackend::init() {
+ if (initialized_) {
+ return;
+ }
+
+ 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 = MiniaudioBackend::audio_callback;
+ config.pUserData = this;
+
+ if (ma_device_init(NULL, &config, &device_) != MA_SUCCESS) {
+ printf("Failed to open playback device.\n");
+ return;
+ }
+
+ initialized_ = true;
+}
+
+void MiniaudioBackend::start() {
+ if (!initialized_) {
+ printf("Cannot start: backend not initialized.\n");
+ return;
+ }
+
+ if (ma_device_start(&device_) != MA_SUCCESS) {
+ printf("Failed to start playback device.\n");
+ ma_device_uninit(&device_);
+ initialized_ = false;
+ return;
+ }
+}
+
+void MiniaudioBackend::shutdown() {
+ if (!initialized_) {
+ return;
+ }
+
+ ma_device_stop(&device_);
+ ma_device_uninit(&device_);
+ initialized_ = false;
+}
diff --git a/src/audio/miniaudio_backend.h b/src/audio/miniaudio_backend.h
new file mode 100644
index 0000000..d46a0c5
--- /dev/null
+++ b/src/audio/miniaudio_backend.h
@@ -0,0 +1,31 @@
+// This file is part of the 64k demo project.
+// It implements the production audio backend using miniaudio.
+// This is the default backend for the final build.
+
+#pragma once
+
+#include "audio_backend.h"
+#include "miniaudio.h"
+
+// Production audio backend using miniaudio library
+// Manages real hardware audio device and playback
+class MiniaudioBackend : public AudioBackend {
+ public:
+ MiniaudioBackend();
+ ~MiniaudioBackend() override;
+
+ void init() override;
+ void start() override;
+ void shutdown() override;
+
+ // Get the underlying miniaudio device (for internal use)
+ ma_device* get_device() { return &device_; }
+
+ private:
+ ma_device device_;
+ bool initialized_;
+
+ // 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/synth.cc b/src/audio/synth.cc
index db5a96c..67bc46e 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -10,6 +10,11 @@
#include <stdio.h> // For printf
#include <string.h> // For memset
+#if !defined(STRIP_ALL)
+#include "audio/audio.h"
+#include "audio/audio_backend.h"
+#endif /* !defined(STRIP_ALL) */
+
struct Voice {
bool active;
int spectrogram_id;
@@ -37,10 +42,17 @@ static volatile float g_current_output_peak =
0.0f; // Global peak for visualization
static float g_hamming_window[WINDOW_SIZE]; // Static window for optimization
+#if !defined(STRIP_ALL)
+static float g_elapsed_time_sec = 0.0f; // Tracks elapsed time for event hooks
+#endif /* !defined(STRIP_ALL) */
+
void synth_init() {
memset(&g_synth_data, 0, sizeof(g_synth_data));
memset(g_voices, 0, sizeof(g_voices));
g_current_output_peak = 0.0f;
+#if !defined(STRIP_ALL)
+ g_elapsed_time_sec = 0.0f;
+#endif /* !defined(STRIP_ALL) */
// Initialize the Hamming window once
hamming_window_512(g_hamming_window);
}
@@ -127,6 +139,15 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan) {
v.active_spectral_data =
g_synth_data.active_spectrogram_data[spectrogram_id];
+#if !defined(STRIP_ALL)
+ // Notify backend of voice trigger event (for testing/tracking)
+ AudioBackend* backend = audio_get_backend();
+ if (backend != nullptr) {
+ backend->on_voice_triggered(g_elapsed_time_sec, spectrogram_id, volume,
+ pan);
+ }
+#endif /* !defined(STRIP_ALL) */
+
return; // Voice triggered
}
}
@@ -188,6 +209,12 @@ void synth_render(float* output_buffer, int num_frames) {
g_current_output_peak = fmaxf(
g_current_output_peak, fmaxf(fabsf(left_sample), fabsf(right_sample)));
}
+
+#if !defined(STRIP_ALL)
+ // Update elapsed time for event tracking (32000 Hz sample rate)
+ const float sample_rate = 32000.0f;
+ g_elapsed_time_sec += (float)num_frames / sample_rate;
+#endif /* !defined(STRIP_ALL) */
}
int synth_get_active_voice_count() {