summaryrefslogtreecommitdiff
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
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>
-rw-r--r--CMakeLists.txt28
-rw-r--r--TODO.md221
-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
-rw-r--r--src/tests/test_audio_backend.cc119
9 files changed, 580 insertions, 37 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 350abdf..03ce641 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,8 +77,8 @@ elseif (NOT DEMO_CROSS_COMPILE_WIN32)
list(APPEND DEMO_LIBS pthread m dl)
endif()
-#-- - Source Groups -- -
-set(AUDIO_SOURCES src/audio/audio.cc src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc src/audio/synth.cc src/audio/tracker.cc)
+#-- - Source Groups -- -
+set(AUDIO_SOURCES src/audio/audio.cc src/audio/miniaudio_backend.cc src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc src/audio/synth.cc src/audio/tracker.cc)
set(PROCEDURAL_SOURCES src/procedural/generator.cc)
set(GPU_SOURCES
src/gpu/gpu.cc
@@ -264,19 +264,27 @@ endif()
#-- - Tests -- -
enable_testing()
if(DEMO_BUILD_TESTS)
- add_demo_test(test_window HammingWindowTest src/tests/test_window.cc)
- target_link_libraries(test_window PRIVATE audio ${DEMO_LIBS})
+ add_demo_test(test_window HammingWindowTest src/tests/test_window.cc ${GEN_DEMO_CC})
+ target_link_libraries(test_window PRIVATE audio util procedural ${DEMO_LIBS})
+ add_dependencies(test_window generate_demo_assets)
add_demo_test(test_maths MathUtilsTest src/tests/test_maths.cc)
- add_demo_test(test_synth SynthEngineTest src/tests/test_synth.cc)
- target_link_libraries(test_synth PRIVATE audio ${DEMO_LIBS})
+ add_demo_test(test_synth SynthEngineTest src/tests/test_synth.cc ${GEN_DEMO_CC})
+ target_link_libraries(test_synth PRIVATE audio util procedural ${DEMO_LIBS})
+ add_dependencies(test_synth generate_demo_assets)
- add_demo_test(test_dct DctTest src/tests/test_dct.cc)
- target_link_libraries(test_dct PRIVATE audio ${DEMO_LIBS})
+ add_demo_test(test_dct DctTest src/tests/test_dct.cc ${GEN_DEMO_CC})
+ target_link_libraries(test_dct PRIVATE audio util procedural ${DEMO_LIBS})
+ add_dependencies(test_dct generate_demo_assets)
- add_demo_test(test_audio_gen AudioGenTest src/tests/test_audio_gen.cc)
- target_link_libraries(test_audio_gen PRIVATE audio ${DEMO_LIBS})
+ add_demo_test(test_audio_gen AudioGenTest src/tests/test_audio_gen.cc ${GEN_DEMO_CC})
+ target_link_libraries(test_audio_gen PRIVATE audio util procedural ${DEMO_LIBS})
+ add_dependencies(test_audio_gen generate_demo_assets)
+
+ add_demo_test(test_audio_backend AudioBackendTest src/tests/test_audio_backend.cc ${GEN_DEMO_CC})
+ target_link_libraries(test_audio_backend PRIVATE audio util procedural ${DEMO_LIBS})
+ add_dependencies(test_audio_backend generate_demo_assets)
add_demo_test(test_tracker TrackerSystemTest src/tests/test_tracker.cc ${GEN_DEMO_CC} ${GENERATED_MUSIC_DATA_CC})
target_link_libraries(test_tracker PRIVATE audio util procedural ${DEMO_LIBS})
diff --git a/TODO.md b/TODO.md
index fd76e69..a981f8e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,6 +3,15 @@
This file tracks prioritized tasks with detailed attack plans.
## Recently Completed (February 4, 2026)
+- [x] **Task #51.1: Audio Backend Abstraction**:
+ - [x] **Interface Created**: Defined `AudioBackend` interface in `src/audio/audio_backend.h` with hooks for voice triggering and frame rendering.
+ - [x] **Production Backend**: Moved miniaudio implementation to `MiniaudioBackend` class, maintaining backward compatibility.
+ - [x] **Audio Refactoring**: Updated `audio.cc` to use backend abstraction with automatic fallback to `MiniaudioBackend`.
+ - [x] **Event Hooks**: Added time tracking to `synth.cc` with `on_voice_triggered()` callbacks (guarded by `!STRIP_ALL`).
+ - [x] **Verification Test**: Created `test_audio_backend.cc` to verify backend injection and event recording work correctly.
+ - [x] **Build Integration**: Updated CMakeLists.txt to include new backend files and link audio tests properly with util/procedural dependencies.
+ - [x] **Zero Size Impact**: All test infrastructure under `#if !defined(STRIP_ALL)`, production path unchanged.
+
- [x] **Task #50: WGSL Modularization**:
- [x] **Recursive Composition**: Updated `ShaderComposer` to support recursive `#include "snippet_name"` directives with cycle detection.
- [x] **Granular SDF Library**: Extracted `math/sdf_shapes.wgsl`, `math/sdf_utils.wgsl`, `render/shadows.wgsl`, `render/scene_query.wgsl`, and `render/lighting_utils.wgsl`.
@@ -61,7 +70,12 @@ This file tracks prioritized tasks with detailed attack plans.
## Priority 4: Developer Tooling & CI
**Goal**: Improve developer workflows, code quality, and release processes.
-*(No active tasks)*
+
+- [ ] **Task #51: Tracker Timing Verification**
+ - [x] **Task #51.1: Audio Backend Abstraction**: Create an interface to separate audio output from synth logic, enabling testable backends.
+ - [ ] **Task #51.2: Mock Audio Backend**: Implement a test backend that records voice trigger events with precise timestamps.
+ - [ ] **Task #51.3: Tracker Test Suite**: Create `test_tracker.cc` to verify pattern triggering, timing accuracy, and synchronization.
+ - [ ] **Task #51.4: Integration with Build**: Wire up tests to CMake and ensure they run in CI.
## Phase 2: Size Optimization (Final Goal)
@@ -73,4 +87,209 @@ This file tracks prioritized tasks with detailed attack plans.
- [ ] **Task #35: CRT Replacement**: investigation and implementation of CRT-free entry point.
+---
+
+## Task #51: Tracker Timing Verification - Detailed Attack Plan
+
+**Problem Statement**: The tracker and synthesizer have audio sync issues. There's no robust way to verify that tracker patterns trigger at the correct timestamps without running the full audio hardware stack.
+
+**Goal**: Implement a testable audio backend abstraction with event recording capabilities to verify tracker timing accuracy.
+
+### Task #51.1: Audio Backend Abstraction
+**Objective**: Decouple audio output from synthesis logic to enable testing without hardware.
+
+**Implementation Steps**:
+- [ ] **Create `src/audio/audio_backend.h`**:
+ - Define `AudioBackend` interface with pure virtual methods:
+ - `init()`: Initialize backend resources
+ - `start()`: Start audio playback/recording
+ - `shutdown()`: Clean up resources
+ - `on_voice_triggered(timestamp, spec_id, volume, pan)`: Hook for voice events
+ - Add `#if !defined(STRIP_ALL)` guards around test-only methods
+
+- [ ] **Create `src/audio/miniaudio_backend.h` and `.cc`**:
+ - Move current miniaudio implementation from `audio.cc` to `MiniaudioBackend` class
+ - Implement `AudioBackend` interface
+ - Keep production behavior identical (no regressions)
+ - This backend does NOT record events (production path)
+
+- [ ] **Refactor `src/audio/audio.cc`**:
+ - Add global `AudioBackend* g_audio_backend` pointer
+ - Add `void audio_set_backend(AudioBackend* backend)` function (under `!STRIP_ALL`)
+ - Default to `MiniaudioBackend` if no backend is set
+ - Replace direct miniaudio calls with backend interface calls
+
+- [ ] **Update `src/audio/synth.cc`**:
+ - Add external hook: `extern AudioBackend* g_audio_backend_for_events`
+ - In `synth_trigger_voice()`, call `backend->on_voice_triggered()` if backend exists
+ - Ensure hook is `#if !defined(STRIP_ALL)` guarded
+
+**Size Impact**: Zero (test code stripped in final build).
+
+**Validation**: Existing tests (`test_synth.cc`) must pass unchanged.
+
+---
+
+### Task #51.2: Mock Audio Backend
+**Objective**: Create a test-only backend that records all audio events with timestamps.
+
+**Implementation Steps**:
+- [ ] **Create `src/audio/mock_audio_backend.h` and `.cc`** (under `#if !defined(STRIP_ALL)`):
+ - Define `struct VoiceTriggerEvent`:
+ ```cpp
+ struct VoiceTriggerEvent {
+ float timestamp_sec;
+ int spectrogram_id;
+ float volume;
+ float pan;
+ };
+ ```
+ - Implement `MockAudioBackend` class:
+ - Maintain `std::vector<VoiceTriggerEvent> recorded_events`
+ - Override `on_voice_triggered()` to record events with current time
+ - Add `const std::vector<VoiceTriggerEvent>& get_events() const`
+ - Add `void clear_events()`
+ - Add `void advance_time(float delta_sec)` to simulate time progression
+ - Implement `init()`, `start()`, `shutdown()` as no-ops
+
+- [ ] **Time Tracking**:
+ - Add `float current_time_sec` member to `MockAudioBackend`
+ - Increment `current_time_sec` in `advance_time()`
+ - Use `current_time_sec` as timestamp when recording events
+
+- [ ] **Synth Integration**:
+ - When `synth_render()` is called, calculate frames rendered
+ - Notify backend of time elapsed: `frames / sample_rate`
+ - Update mock's internal clock accordingly
+
+**Testing**: Create minimal unit test in `test_tracker.cc` to verify event recording works.
+
+---
+
+### Task #51.3: Tracker Test Suite
+**Objective**: Comprehensive tests for tracker pattern triggering and timing accuracy.
+
+**Implementation Steps**:
+- [ ] **Create `src/tests/test_tracker.cc`**:
+
+ - **Test 1: Single Pattern Trigger**
+ - Define a minimal `TrackerScore` with 1 pattern at `t=1.0s`
+ - Set up mock backend
+ - Call `tracker_update(0.5)` → verify no events
+ - Call `tracker_update(1.0)` → verify 1 event recorded
+ - Validate event timestamp matches expected trigger time
+
+ - **Test 2: Multiple Pattern Triggers**
+ - Score with 3 patterns at `t=0.5s, 1.0s, 2.0s`
+ - Progressively call `tracker_update()` with increasing times
+ - Verify each pattern triggers exactly once at correct time
+
+ - **Test 3: Event Timing Accuracy**
+ - Pattern with multiple events at different beat offsets
+ - Verify each event's timestamp matches: `pattern_start_time + (beat * beat_duration)`
+ - Use tolerance: `±1 frame` (1/32000 sec ≈ 31.25µs)
+
+ - **Test 4: BPM Scaling**
+ - Same pattern tested at different BPMs (60, 120, 180)
+ - Verify beat-to-time conversion is accurate: `beat_sec = 60.0 / bpm`
+
+ - **Test 5: Pattern Overlap**
+ - Two patterns with overlapping time ranges
+ - Verify both trigger correctly without interference
+
+ - **Test 6: Asset vs Procedural Samples**
+ - Pattern using both asset-based spectrograms and procedural notes
+ - Verify both types render and trigger correctly
+
+ - **Test 7: Seek/Fast-Forward Simulation**
+ - Simulate `audio_render_silent()` behavior
+ - Start at `t=0`, fast-forward to `t=10.0s`
+ - Verify all patterns in range [0, 10] triggered correctly
+
+- [ ] **Helper Functions**:
+ ```cpp
+ void assert_event_at_time(const std::vector<VoiceTriggerEvent>& events,
+ float expected_time, float tolerance = 0.001f);
+
+ void assert_event_count(const std::vector<VoiceTriggerEvent>& events,
+ int expected_count);
+
+ TrackerScore create_test_score(const std::vector<float>& trigger_times,
+ float bpm = 120.0f);
+ ```
+
+**Coverage Target**: 95%+ for `src/audio/tracker.cc`.
+
+---
+
+### Task #51.4: Integration with Build
+**Objective**: Wire up tests to CMake and ensure they run automatically.
+
+**Implementation Steps**:
+- [ ] **Update `src/CMakeLists.txt`**:
+ - Add `mock_audio_backend.cc` to test-only sources (under `DEMO_BUILD_TESTS`)
+ - Link `test_tracker` executable with audio subsystem library
+
+- [ ] **Add test to CTest**:
+ ```cmake
+ if(DEMO_BUILD_TESTS)
+ add_executable(test_tracker tests/test_tracker.cc audio/mock_audio_backend.cc)
+ target_link_libraries(test_tracker PRIVATE audio_lib util_lib)
+ add_test(NAME TrackerTest COMMAND test_tracker)
+ endif()
+ ```
+
+- [ ] **Verify in CI**:
+ - Run `cmake --build build && cd build && ctest`
+ - Ensure `test_tracker` runs and passes
+ - Check coverage report includes tracker.cc
+
+**Validation**: `ctest` output shows `TrackerTest: PASSED`.
+
+---
+
+## Implementation Layout Summary
+
+### New Files
+```
+src/audio/audio_backend.h # Interface definition
+src/audio/miniaudio_backend.h # Production backend (header)
+src/audio/miniaudio_backend.cc # Production backend (impl)
+src/audio/mock_audio_backend.h # Test backend (header, !STRIP_ALL)
+src/audio/mock_audio_backend.cc # Test backend (impl, !STRIP_ALL)
+src/tests/test_tracker.cc # Comprehensive tracker tests
+```
+
+### Modified Files
+```
+src/audio/audio.cc # Backend abstraction layer
+src/audio/synth.cc # Add event hooks
+src/CMakeLists.txt # Add new files and tests
+```
+
+### File Structure
+```
+src/audio/
+├── audio_backend.h [NEW] Interface (50 lines)
+├── miniaudio_backend.h [NEW] Header (30 lines)
+├── miniaudio_backend.cc [NEW] Production impl (~100 lines, moved from audio.cc)
+├── mock_audio_backend.h [NEW] Test header (60 lines)
+├── mock_audio_backend.cc [NEW] Test impl (~120 lines)
+├── audio.h [MODIFIED] Add backend setter
+├── audio.cc [MODIFIED] Use backend abstraction (~30 lines changed)
+├── synth.cc [MODIFIED] Add event hook (~10 lines)
+└── tracker.cc [NO CHANGE]
+
+src/tests/
+└── test_tracker.cc [NEW] Test suite (~400 lines)
+```
+
+### Code Organization Principles
+1. **Zero Size Impact**: All test infrastructure under `#if !defined(STRIP_ALL)`
+2. **Backward Compatible**: Production path unchanged, existing tests pass
+3. **Clean Separation**: Interface-based design, easy to add more backends later
+4. **Testable**: Mock backend has minimal dependencies (no hardware/threads)
+
+---
+
## Future Goals
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() {
diff --git a/src/tests/test_audio_backend.cc b/src/tests/test_audio_backend.cc
new file mode 100644
index 0000000..050956b
--- /dev/null
+++ b/src/tests/test_audio_backend.cc
@@ -0,0 +1,119 @@
+// This file is part of the 64k demo project.
+// It tests the audio backend abstraction layer.
+// Verifies backend injection and event hooks work correctly.
+
+#include "audio/audio.h"
+#include "audio/audio_backend.h"
+#include "audio/synth.h"
+#include <assert.h>
+#include <stdio.h>
+#include <vector>
+
+#if !defined(STRIP_ALL)
+
+// Simple test backend that records events
+class TestBackend : public AudioBackend {
+ public:
+ struct Event {
+ float timestamp;
+ int spectrogram_id;
+ float volume;
+ float pan;
+ };
+
+ std::vector<Event> events;
+ bool init_called = false;
+ bool start_called = false;
+ bool shutdown_called = false;
+
+ void init() override { init_called = true; }
+
+ void start() override { start_called = true; }
+
+ void shutdown() override { shutdown_called = true; }
+
+ void on_voice_triggered(float timestamp, int spectrogram_id, float volume,
+ float pan) override {
+ events.push_back({timestamp, spectrogram_id, volume, pan});
+ }
+};
+
+void test_backend_injection() {
+ TestBackend backend;
+
+ // Inject test backend before audio_init
+ audio_set_backend(&backend);
+
+ audio_init();
+ assert(backend.init_called);
+
+ audio_start();
+ assert(backend.start_called);
+
+ audio_shutdown();
+ assert(backend.shutdown_called);
+
+ printf("Backend injection test PASSED\n");
+}
+
+void test_event_recording() {
+ TestBackend backend;
+ audio_set_backend(&backend);
+
+ synth_init();
+
+ // Create a dummy spectrogram
+ float data[DCT_SIZE * 2] = {0};
+ Spectrogram spec = {data, data, 2};
+ int id = synth_register_spectrogram(&spec);
+
+ // Trigger a voice
+ synth_trigger_voice(id, 0.8f, -0.5f);
+
+ // Render some frames to advance time
+ float output[1024] = {0};
+ synth_render(output, 256); // ~0.008 sec at 32kHz
+
+ // Verify event was recorded
+ assert(backend.events.size() == 1);
+ assert(backend.events[0].spectrogram_id == id);
+ assert(backend.events[0].volume == 0.8f);
+ assert(backend.events[0].pan == -0.5f);
+ assert(backend.events[0].timestamp == 0.0f); // Triggered before any render
+
+ // Trigger another voice after rendering
+ synth_trigger_voice(id, 1.0f, 0.0f);
+
+ assert(backend.events.size() == 2);
+ assert(backend.events[1].timestamp > 0.0f); // Should be > 0 now
+
+ printf("Event recording test PASSED\n");
+}
+
+void test_default_backend() {
+ // Reset backend to nullptr to test default
+ audio_set_backend(nullptr);
+
+ // This should use MiniaudioBackend by default
+ audio_init();
+ audio_start();
+ audio_shutdown();
+
+ printf("Default backend test PASSED\n");
+}
+
+#endif /* !defined(STRIP_ALL) */
+
+int main() {
+#if !defined(STRIP_ALL)
+ printf("Running Audio Backend tests...\n");
+ test_backend_injection();
+ test_event_recording();
+ test_default_backend();
+ printf("All Audio Backend tests PASSED\n");
+ return 0;
+#else
+ printf("Audio Backend tests skipped (STRIP_ALL enabled)\n");
+ return 0;
+#endif /* !defined(STRIP_ALL) */
+}