diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-05 19:13:34 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-05 19:13:34 +0100 |
| commit | 798fc6d471a70ed930e5b1fc084818cb337ca5b1 (patch) | |
| tree | bf89015f733bc7c52e4b9c37593669cb69ae8bd8 /src/audio/spectrogram_resource_manager.cc | |
| parent | 4a3f7a2c379a3e9554e720685e03842180b021ce (diff) | |
feat(audio): Implement AudioEngine and SpectrogramResourceManager (Task #56 Phase 1)
Implements Phase 1 of the audio lifecycle refactor to eliminate initialization
order dependencies between synth and tracker.
New Components:
1. SpectrogramResourceManager (src/audio/spectrogram_resource_manager.{h,cc})
- Centralized resource loading and ownership
- Lazy loading: resources registered but not loaded until needed
- Handles both asset spectrograms and procedural notes
- Clear ownership: assets borrowed, procedurals owned
- Optional cache eviction under DEMO_ENABLE_CACHE_EVICTION flag
2. AudioEngine (src/audio/audio_engine.{h,cc})
- Unified audio subsystem manager
- Single initialization point eliminates order dependencies
- Manages synth, tracker, and resource manager lifecycle
- Timeline seeking API for debugging (!STRIP_ALL)
- Clean API: init(), shutdown(), reset(), seek()
Features:
- Lazy loading strategy with manual preload API
- Reset functionality for timeline seeking
- Zero impact on production builds
- Debug-only seeking support
Testing:
- Comprehensive test suite (test_audio_engine.cc)
- Tests lifecycle, resource loading, reset, seeking
- All 20 tests passing (100% pass rate)
Bug Fixes:
- Fixed infinite recursion in AudioEngine::tracker_reset()
Integration:
- Added to CMakeLists.txt audio library
- No changes to existing code (backward compatible)
Binary Size Impact: ~700 bytes (within budget)
Next: Phase 2 (Test Migration) - Update existing tests to use AudioEngine
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/audio/spectrogram_resource_manager.cc')
| -rw-r--r-- | src/audio/spectrogram_resource_manager.cc | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/src/audio/spectrogram_resource_manager.cc b/src/audio/spectrogram_resource_manager.cc new file mode 100644 index 0000000..dbed09e --- /dev/null +++ b/src/audio/spectrogram_resource_manager.cc @@ -0,0 +1,228 @@ +// This file is part of the 64k demo project. +// SpectrogramResourceManager implementation. + +#include "spectrogram_resource_manager.h" +#include "audio/audio.h" +#include "procedural/generator.h" +#include "util/debug.h" +#include <cstring> +#include <vector> + +void SpectrogramResourceManager::init() { + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + resources_[i].owned_data = nullptr; + resources_[i].asset_id = AssetId::ASSET_LAST_ID; + resources_[i].state = UNREGISTERED; +#if defined(DEMO_ENABLE_CACHE_EVICTION) + resources_[i].last_access_time = 0.0f; +#endif + } + loaded_count_ = 0; +} + +void SpectrogramResourceManager::shutdown() { + // Free all owned memory (procedural spectrograms) + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + if (resources_[i].owned_data != nullptr) { + delete[] resources_[i].owned_data; + resources_[i].owned_data = nullptr; + } + } + loaded_count_ = 0; +} + +void SpectrogramResourceManager::reset() { + // Clear state but keep registrations (useful for seeking) + // Don't free memory, just mark as unloaded + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + if (resources_[i].state == LOADED) { + resources_[i].state = REGISTERED; + } + } + loaded_count_ = 0; +} + +void SpectrogramResourceManager::register_asset(int sample_id, AssetId asset_id) { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return; + } + + Resource& resource = resources_[sample_id]; + resource.asset_id = asset_id; + resource.state = REGISTERED; + +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr] Registered asset sample_id=%d, asset_id=%d\n", + sample_id, (int)asset_id); +#endif +} + +void SpectrogramResourceManager::register_procedural(int sample_id, const NoteParams& params) { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return; + } + + Resource& resource = resources_[sample_id]; + resource.asset_id = AssetId::ASSET_LAST_ID; // Mark as procedural + resource.proc_params = params; + resource.state = REGISTERED; + +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr] Registered procedural sample_id=%d, freq=%.2f\n", + sample_id, params.base_freq); +#endif +} + +const Spectrogram* SpectrogramResourceManager::get_or_load(int sample_id) { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return nullptr; + } + + Resource& resource = resources_[sample_id]; + + // Already loaded? + if (resource.state == LOADED) { +#if defined(DEMO_ENABLE_CACHE_EVICTION) + resource.last_access_time = 0.0f; // TODO: Get actual time +#endif + return &resource.spec; + } + + // Need to load + if (resource.state == REGISTERED) { + if (resource.asset_id != AssetId::ASSET_LAST_ID) { + load_asset(&resource); + } else { + load_procedural(&resource); + } + resource.state = LOADED; + loaded_count_++; + } + + return (resource.state == LOADED) ? &resource.spec : nullptr; +} + +void SpectrogramResourceManager::preload(int sample_id) { + // Just call get_or_load to trigger loading + get_or_load(sample_id); +} + +void SpectrogramResourceManager::preload_range(int start_id, int end_id) { + for (int i = start_id; i <= end_id && i < MAX_SPECTROGRAM_RESOURCES; ++i) { + preload(i); + } +} + +const Spectrogram* SpectrogramResourceManager::get_spectrogram(int sample_id) const { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return nullptr; + } + + const Resource& resource = resources_[sample_id]; + return (resource.state == LOADED) ? &resource.spec : nullptr; +} + +bool SpectrogramResourceManager::is_loaded(int sample_id) const { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return false; + } + return resources_[sample_id].state == LOADED; +} + +int SpectrogramResourceManager::get_loaded_count() const { + return loaded_count_; +} + +#if defined(DEMO_ENABLE_CACHE_EVICTION) +void SpectrogramResourceManager::release(int sample_id) { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return; + } + + Resource& resource = resources_[sample_id]; + if (resource.state == LOADED && resource.owned_data != nullptr) { + delete[] resource.owned_data; + resource.owned_data = nullptr; + resource.state = EVICTED; + loaded_count_--; + +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr] Released sample_id=%d\n", sample_id); +#endif + } +} + +void SpectrogramResourceManager::release_all() { + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + release(i); + } +} + +void SpectrogramResourceManager::try_evict_lru(float current_time) { + // Find least recently used loaded resource + int lru_id = -1; + float oldest_time = current_time; + + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + if (resources_[i].state == LOADED && resources_[i].last_access_time < oldest_time) { + oldest_time = resources_[i].last_access_time; + lru_id = i; + } + } + + // Evict if not accessed in last 10 seconds + if (lru_id != -1 && (current_time - oldest_time) > 10.0f) { + release(lru_id); + } +} +#endif /* defined(DEMO_ENABLE_CACHE_EVICTION) */ + +void SpectrogramResourceManager::load_asset(Resource* resource) { + size_t size; + const uint8_t* data = GetAsset(resource->asset_id, &size); + + if (data == nullptr || size < sizeof(SpecHeader)) { +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr ERROR] Failed to load asset %d\n", (int)resource->asset_id); +#endif + return; + } + + const SpecHeader* header = (const SpecHeader*)data; + const float* spectral_data = (const float*)(data + sizeof(SpecHeader)); + + resource->spec.spectral_data_a = spectral_data; + resource->spec.spectral_data_b = spectral_data; + resource->spec.num_frames = header->num_frames; + resource->owned_data = nullptr; // Asset data is not owned + +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr] Loaded asset %d: %d frames\n", + (int)resource->asset_id, header->num_frames); +#endif +} + +void SpectrogramResourceManager::load_procedural(Resource* resource) { + int note_frames = 0; + std::vector<float> note_data = generate_note_spectrogram(resource->proc_params, ¬e_frames); + + if (note_frames <= 0 || note_data.empty()) { +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr ERROR] Failed to generate procedural note\n"); +#endif + return; + } + + // Allocate persistent storage + resource->owned_data = new float[note_data.size()]; + memcpy(resource->owned_data, note_data.data(), note_data.size() * sizeof(float)); + + resource->spec.spectral_data_a = resource->owned_data; + resource->spec.spectral_data_b = resource->owned_data; + resource->spec.num_frames = note_frames; + +#if defined(DEBUG_LOG_ASSETS) + DEBUG_ASSETS("[ResourceMgr] Generated procedural: %d frames, freq=%.2f\n", + note_frames, resource->proc_params.base_freq); +#endif +} |
