diff options
Diffstat (limited to 'src/audio')
| -rw-r--r-- | src/audio/audio_engine.cc | 216 | ||||
| -rw-r--r-- | src/audio/audio_engine.h | 67 | ||||
| -rw-r--r-- | src/audio/spectrogram_resource_manager.cc | 228 | ||||
| -rw-r--r-- | src/audio/spectrogram_resource_manager.h | 74 |
4 files changed, 585 insertions, 0 deletions
diff --git a/src/audio/audio_engine.cc b/src/audio/audio_engine.cc new file mode 100644 index 0000000..184e4aa --- /dev/null +++ b/src/audio/audio_engine.cc @@ -0,0 +1,216 @@ +// This file is part of the 64k demo project. +// AudioEngine implementation. + +#include "audio_engine.h" +#include "util/debug.h" +#include <cstring> +#include <algorithm> + +void AudioEngine::init() { + if (initialized_) { + return; + } + + // Initialize in correct order (synth first, then tracker) + synth_init(); + resource_mgr_.init(); + tracker_init(); + + // Initialize sample-to-synth-id mapping + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + sample_to_synth_id_[i] = -1; + } + + current_time_ = 0.0f; + initialized_ = true; + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Initialized\n"); +#endif +} + +void AudioEngine::shutdown() { + if (!initialized_) { + return; + } + + resource_mgr_.shutdown(); + synth_shutdown(); + initialized_ = false; + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Shutdown\n"); +#endif +} + +void AudioEngine::reset() { + if (!initialized_) { + return; + } + + synth_init(); // Re-init synth (clears all state) + tracker_reset(); + resource_mgr_.reset(); + + // Clear sample-to-synth mapping + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + sample_to_synth_id_[i] = -1; + } + + current_time_ = 0.0f; + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Reset\n"); +#endif +} + +void AudioEngine::load_music_data(const TrackerScore* score, + const NoteParams* samples, + const AssetId* sample_assets, + uint32_t sample_count) { + // Register sample metadata (lazy loading - don't load yet!) + for (uint32_t i = 0; i < sample_count; ++i) { + if (sample_assets[i] != AssetId::ASSET_LAST_ID) { + resource_mgr_.register_asset(i, sample_assets[i]); + } else { + resource_mgr_.register_procedural(i, samples[i]); + } + } + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Loaded music data: %u samples\n", sample_count); +#endif +} + +void AudioEngine::update(float music_time) { + current_time_ = music_time; + + // Pre-warm samples needed in next 2 seconds (lazy loading strategy) + // TODO: Implement pre-warming based on upcoming pattern triggers + + // Update tracker (triggers events) + tracker_update(music_time); +} + +void AudioEngine::render(float* output_buffer, int num_frames) { + synth_render(output_buffer, num_frames); +} + +int AudioEngine::get_active_voice_count() const { + return synth_get_active_voice_count(); +} + +void AudioEngine::tracker_reset() { + ::tracker_reset(); +} + +void AudioEngine::trigger_sample(int sample_id, float volume, float pan) { + // Load resource on-demand if not cached + const Spectrogram* spec = resource_mgr_.get_or_load(sample_id); + if (spec == nullptr) { +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine ERROR] Failed to load sample_id=%d\n", sample_id); +#endif + return; + } + + // Register with synth (lazy registration) + const int synth_id = get_or_register_synth_id(sample_id); + if (synth_id == -1) { +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine ERROR] Failed to register sample_id=%d with synth\n", sample_id); +#endif + return; + } + + // Trigger voice + synth_trigger_voice(synth_id, volume, pan); +} + +int AudioEngine::get_or_register_synth_id(int sample_id) { + if (sample_id < 0 || sample_id >= MAX_SPECTROGRAM_RESOURCES) { + return -1; + } + + // Already registered? + if (sample_to_synth_id_[sample_id] != -1) { + return sample_to_synth_id_[sample_id]; + } + + // Get resource (should already be loaded by trigger_sample) + const Spectrogram* spec = resource_mgr_.get_spectrogram(sample_id); + if (spec == nullptr) { + return -1; + } + + // Register with synth + const int synth_id = synth_register_spectrogram(spec); + sample_to_synth_id_[sample_id] = synth_id; + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Registered sample_id=%d → synth_id=%d\n", sample_id, synth_id); +#endif + + return synth_id; +} + +#if !defined(STRIP_ALL) +void AudioEngine::seek(float target_time) { + if (!initialized_) { + return; + } + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Seeking to t=%.3fs (from t=%.3fs)\n", target_time, current_time_); +#endif + + // 1. Reset synth state (clear all active voices) + synth_init(); + + // 2. Reset tracker state + tracker_reset(); + + // 3. Clear sample-to-synth mapping (will be re-registered on demand) + for (int i = 0; i < MAX_SPECTROGRAM_RESOURCES; ++i) { + sample_to_synth_id_[i] = -1; + } + + // 4. Pre-warm samples for target time range + const float prewarm_start = std::max(0.0f, target_time - 1.0f); + const float prewarm_end = target_time + 2.0f; + prewarm_for_time_range(prewarm_start, prewarm_end); + + // 5. Simulate tracker up to target time (without audio) + const float dt = 0.1f; + for (float t = 0.0f; t < target_time; t += dt) { + update_silent(t); + } + + // 6. Final update at exact target time + tracker_update(target_time); + current_time_ = target_time; + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Seek complete, loaded samples: %d\n", + resource_mgr_.get_loaded_count()); +#endif +} + +void AudioEngine::prewarm_for_time_range(float start_time, float end_time) { + // TODO: Scan tracker score for patterns in this time range + // and pre-load their samples. For now, this is a placeholder. + // The proper implementation requires access to g_tracker_score + // and pattern data to determine which samples will be needed. + +#if defined(DEBUG_LOG_AUDIO) + DEBUG_AUDIO("[AudioEngine] Pre-warming samples for t=%.2f-%.2f\n", start_time, end_time); +#endif +} + +void AudioEngine::update_silent(float music_time) { + // Update tracker without triggering audio (for fast-forward/seeking) + // This is a placeholder - proper implementation requires tracker support + // for silent updates. For now, we just update normally. + tracker_update(music_time); +} +#endif /* !defined(STRIP_ALL) */ diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h new file mode 100644 index 0000000..ef3c163 --- /dev/null +++ b/src/audio/audio_engine.h @@ -0,0 +1,67 @@ +// This file is part of the 64k demo project. +// AudioEngine: Unified audio subsystem manager. +// Eliminates initialization order dependencies between synth and tracker. + +#pragma once + +#include "audio/spectrogram_resource_manager.h" +#include "audio/synth.h" +#include "audio/tracker.h" + +// Unified audio engine managing synth, tracker, and resources +class AudioEngine { + public: + // Lifecycle + void init(); + void shutdown(); + void reset(); // Clear all state (for seeking/rewinding) + + // Music data loading + void load_music_data(const TrackerScore* score, + const NoteParams* samples, + const AssetId* sample_assets, + uint32_t sample_count); + + // Update loop + void update(float music_time); + +#if !defined(STRIP_ALL) + // Timeline seeking (debugging only) + void seek(float target_time); + float get_time() const { return current_time_; } +#endif /* !defined(STRIP_ALL) */ + + // Synth interface (delegates to internal synth) + void render(float* output_buffer, int num_frames); + int get_active_voice_count() const; + + // Tracker interface + void tracker_reset(); + + // Resource access + SpectrogramResourceManager* get_resource_manager() { return &resource_mgr_; } + + private: + // Trigger a sample (loads resource if needed, registers with synth) + void trigger_sample(int sample_id, float volume, float pan); + + // Get or create synth ID for a sample + int get_or_register_synth_id(int sample_id); + +#if !defined(STRIP_ALL) + // Seeking support + void prewarm_for_time_range(float start_time, float end_time); + void update_silent(float music_time); // Update without triggering audio +#endif + + SpectrogramResourceManager resource_mgr_; + + // NOTE: For now, synth and tracker are global C functions (not members) + // Future refactoring will convert them to member objects + + // Mapping: sample_id → synth_id (lazy registration) + int sample_to_synth_id_[MAX_SPECTROGRAM_RESOURCES]; + + float current_time_ = 0.0f; + bool initialized_ = false; +}; 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 +} diff --git a/src/audio/spectrogram_resource_manager.h b/src/audio/spectrogram_resource_manager.h new file mode 100644 index 0000000..97196d1 --- /dev/null +++ b/src/audio/spectrogram_resource_manager.h @@ -0,0 +1,74 @@ +// This file is part of the 64k demo project. +// SpectrogramResourceManager: Centralized resource loading and ownership. +// Handles both asset spectrograms and procedurally generated notes. + +#pragma once + +#include "audio/gen.h" +#include "audio/synth.h" +#include "generated/assets.h" +#include "util/asset_manager.h" +#include <cstdint> + +// Maximum number of unique spectrogram resources +constexpr int MAX_SPECTROGRAM_RESOURCES = 256; + +class SpectrogramResourceManager { + public: + // Lifecycle + void init(); + void shutdown(); // Frees all owned memory + void reset(); // Clear state but keep registrations + + // Metadata registration (no loading yet, just bookkeeping) + void register_asset(int sample_id, AssetId asset_id); + void register_procedural(int sample_id, const NoteParams& params); + + // Lazy loading API (loads on first access if not cached) + const Spectrogram* get_or_load(int sample_id); + + // Explicit pre-warming (for timeline seeking) + void preload(int sample_id); + void preload_range(int start_id, int end_id); + + // Query API + const Spectrogram* get_spectrogram(int sample_id) const; + bool is_loaded(int sample_id) const; + int get_loaded_count() const; + +#if defined(DEMO_ENABLE_CACHE_EVICTION) + // Cache management (optional, compile-time flag) + void release(int sample_id); + void release_all(); + void try_evict_lru(float current_time); +#endif /* defined(DEMO_ENABLE_CACHE_EVICTION) */ + + private: + enum ResourceState { + UNREGISTERED = 0, // No metadata registered + REGISTERED, // Metadata registered, not loaded yet + LOADED, // Fully loaded and ready +#if defined(DEMO_ENABLE_CACHE_EVICTION) + EVICTED // Was loaded, now evicted +#endif + }; + + struct Resource { + Spectrogram spec; + float* owned_data; // nullptr if asset (not owned), allocated if procedural + AssetId asset_id; // ASSET_LAST_ID if procedural + NoteParams proc_params; + ResourceState state; + +#if defined(DEMO_ENABLE_CACHE_EVICTION) + float last_access_time; +#endif + }; + + // Load implementation + void load_asset(Resource* resource); + void load_procedural(Resource* resource); + + Resource resources_[MAX_SPECTROGRAM_RESOURCES]; + int loaded_count_ = 0; +}; |
