summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/audio.cc31
-rw-r--r--src/audio/miniaudio_backend.cc164
-rw-r--r--src/audio/ring_buffer.cc63
-rw-r--r--src/audio/ring_buffer.h9
-rw-r--r--src/audio/synth.cc77
-rw-r--r--src/audio/synth.h14
-rw-r--r--src/audio/tracker.cc155
7 files changed, 437 insertions, 76 deletions
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
index 3b7b7fd..6e1e9b7 100644
--- a/src/audio/audio.cc
+++ b/src/audio/audio.cc
@@ -109,10 +109,12 @@ void audio_render_ahead(float music_time, float dt) {
}
g_pending_samples = remaining;
- // Notify backend
+ // Notify backend (for testing/tracking)
+#if !defined(STRIP_ALL)
if (g_audio_backend != nullptr) {
g_audio_backend->on_frames_rendered(written / RING_BUFFER_CHANNELS);
}
+#endif
}
// If still have pending samples, buffer is full - wait for consumption
@@ -127,25 +129,30 @@ void audio_render_ahead(float music_time, float dt) {
// Stop if buffer is full enough
if (buffered_time >= target_lookahead) break;
- // Check if buffer has space for this chunk
+ // Check available space and render chunk that fits
const int available_space = g_ring_buffer.available_write();
- if (available_space < chunk_samples) {
- // Buffer is too full, wait for audio callback to consume more
+ if (available_space == 0) {
+ // Buffer is completely full, wait for audio callback to consume
break;
}
+ // Determine how much we can actually render
+ // Render the smaller of: desired chunk size OR available space
+ const int actual_samples = (available_space < chunk_samples) ? available_space : chunk_samples;
+ const int actual_frames = actual_samples / RING_BUFFER_CHANNELS;
+
// Allocate temporary buffer (stereo)
- float* temp_buffer = new float[chunk_samples];
+ float* temp_buffer = new float[actual_samples];
// Render audio from synth (advances synth state incrementally)
- synth_render(temp_buffer, chunk_frames);
+ synth_render(temp_buffer, actual_frames);
// Write to ring buffer
- const int written = g_ring_buffer.write(temp_buffer, chunk_samples);
+ const int written = g_ring_buffer.write(temp_buffer, actual_samples);
// If partial write, save remaining samples to pending buffer
- if (written < chunk_samples) {
- const int remaining = chunk_samples - written;
+ if (written < actual_samples) {
+ const int remaining = actual_samples - written;
if (remaining <= MAX_PENDING_SAMPLES) {
for (int i = 0; i < remaining; ++i) {
g_pending_buffer[i] = temp_buffer[written + i];
@@ -155,14 +162,16 @@ void audio_render_ahead(float music_time, float dt) {
}
// Notify backend of frames rendered (count frames sent to synth)
+#if !defined(STRIP_ALL)
if (g_audio_backend != nullptr) {
- g_audio_backend->on_frames_rendered(chunk_frames);
+ g_audio_backend->on_frames_rendered(actual_frames);
}
+#endif
delete[] temp_buffer;
// If we couldn't write everything, stop and retry next frame
- if (written < chunk_samples) break;
+ if (written < actual_samples) break;
}
}
diff --git a/src/audio/miniaudio_backend.cc b/src/audio/miniaudio_backend.cc
index 9dd2b50..c2db268 100644
--- a/src/audio/miniaudio_backend.cc
+++ b/src/audio/miniaudio_backend.cc
@@ -5,22 +5,140 @@
#include "miniaudio_backend.h"
#include "audio.h"
#include "ring_buffer.h"
+#include "util/debug.h"
#include <stdio.h>
+#include <stdlib.h> // for abort()
// 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;
+
+#if defined(DEBUG_LOG_AUDIO)
+ // Validate callback parameters
+ static ma_uint32 last_frameCount = 0;
+ static int callback_reentry = 0;
+ static double last_time = 0.0;
+ static int timing_initialized = 0;
+ static uint64_t total_frames_requested = 0;
+ static uint64_t callback_number = 0;
+
+ callback_number++;
+ total_frames_requested += frameCount;
+
+ // Track timing
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ double now = ts.tv_sec + ts.tv_nsec / 1000000000.0;
+
+ if (timing_initialized) {
+ double delta = (now - last_time) * 1000.0; // ms
+ double expected = ((double)frameCount / pDevice->sampleRate) * 1000.0;
+ double jitter = delta - expected;
+
+ // Enhanced logging: Log first 20 callbacks in detail, then periodic summary
+ if (callback_number <= 20 || callback_number % 50 == 0) {
+ const double elapsed_by_frames = (double)total_frames_requested / pDevice->sampleRate * 1000.0;
+ const double elapsed_by_time = now * 1000.0; // Convert to ms
+ DEBUG_AUDIO("[CB#%llu] frameCount=%u, Delta=%.2fms, Expected=%.2fms, Jitter=%.2fms, "
+ "TotalFrames=%llu (%.1fms), TotalTime=%.1fms, Drift=%.2fms\n",
+ callback_number, frameCount, delta, expected, jitter,
+ total_frames_requested, elapsed_by_frames, elapsed_by_time,
+ elapsed_by_time - elapsed_by_frames);
+ }
+
+ // Detect large timing anomalies (>5ms off from expected)
+ if (fabs(jitter) > 5.0) {
+ DEBUG_AUDIO("[TIMING ANOMALY] CB#%llu Delta=%.2fms, Expected=%.2fms, Jitter=%.2fms\n",
+ callback_number, delta, expected, jitter);
+ }
+ }
+ last_time = now;
+ timing_initialized = 1;
+
+ // Check for re-entrant calls
+ if (callback_reentry > 0) {
+ DEBUG_AUDIO("FATAL: Callback re-entered! depth=%d\n", callback_reentry);
+ abort();
+ }
+ callback_reentry++;
+
+ // Check if frameCount changed unexpectedly
+ if (last_frameCount != 0 && frameCount != last_frameCount) {
+ DEBUG_AUDIO("WARNING: frameCount changed! was=%u, now=%u\n",
+ last_frameCount, frameCount);
+ }
+ last_frameCount = frameCount;
+
+ // Validate device state
+ if (!pDevice || pDevice->sampleRate == 0) {
+ DEBUG_AUDIO("FATAL: Invalid device in callback!\n");
+ abort();
+ }
+
+ // Check actual sample rate matches our expectation
+ if (pDevice->sampleRate != 32000) {
+ static int rate_warning = 0;
+ if (rate_warning++ == 0) {
+ DEBUG_AUDIO("WARNING: Device sample rate is %u, not 32000! Resampling may occur.\n",
+ pDevice->sampleRate);
+ }
+ }
+#endif /* defined(DEBUG_LOG_AUDIO) */
+
float* fOutput = (float*)pOutput;
+ // BOUNDS CHECK: Sanity check on frameCount
+ if (frameCount > 8192 || frameCount == 0) {
+ fprintf(stderr, "AUDIO CALLBACK ERROR: frameCount=%u (unreasonable!)\n", frameCount);
+ abort();
+ }
+
// Read from ring buffer instead of calling synth directly
AudioRingBuffer* ring_buffer = audio_get_ring_buffer();
if (ring_buffer != nullptr) {
const int samples_to_read = (int)frameCount * 2; // Stereo
- ring_buffer->read(fOutput, samples_to_read);
+
+#if defined(DEBUG_LOG_RING_BUFFER)
+ // Track buffer level and detect drops
+ static int min_available = 99999;
+ const int available = ring_buffer->available_read();
+
+ if (available < min_available) {
+ min_available = available;
+ DEBUG_RING_BUFFER("[BUFFER] CB#%llu NEW MIN: available=%d (%.1fms)\n",
+ callback_number, available, (float)available / (32000.0f * 2.0f) * 1000.0f);
+ }
+
+ // Log buffer state for first 20 callbacks and periodically
+ if (callback_number <= 20 || callback_number % 50 == 0) {
+ DEBUG_RING_BUFFER("[BUFFER] CB#%llu requested=%d, available=%d (%.1fms), min=%d\n",
+ callback_number, samples_to_read, available,
+ (float)available / (32000.0f * 2.0f) * 1000.0f, min_available);
+ }
+
+ // CRITICAL: Verify we have enough samples
+ if (available < samples_to_read) {
+ DEBUG_RING_BUFFER("[BUFFER UNDERRUN] CB#%llu requested=%d, available=%d, SHORT=%d\n",
+ callback_number, samples_to_read, available, samples_to_read - available);
+ }
+#endif /* defined(DEBUG_LOG_RING_BUFFER) */
+
+ const int actually_read = ring_buffer->read(fOutput, samples_to_read);
+
+#if defined(DEBUG_LOG_RING_BUFFER)
+ if (actually_read < samples_to_read) {
+ DEBUG_RING_BUFFER("[PARTIAL READ] CB#%llu requested=%d, got=%d, padded=%d with silence\n",
+ callback_number, samples_to_read, actually_read, samples_to_read - actually_read);
+ }
+#endif /* defined(DEBUG_LOG_RING_BUFFER) */
}
+
+#if defined(DEBUG_LOG_AUDIO)
+ // Clear reentry flag
+ callback_reentry--;
+#endif /* defined(DEBUG_LOG_AUDIO) */
}
MiniaudioBackend::MiniaudioBackend() : initialized_(false) {
@@ -41,6 +159,21 @@ void MiniaudioBackend::init() {
config.playback.format = ma_format_f32;
config.playback.channels = 2;
config.sampleRate = 32000;
+
+ // Core Audio Backend-Specific Configuration
+ // Problem: Core Audio uses 10ms periods optimized for 44.1kHz, causing
+ // uneven callback timing (10ms/10ms/20ms) when resampling to 32kHz
+ //
+ // Solution 1: Force OS-level sample rate to 32kHz to avoid resampling
+ config.coreaudio.allowNominalSampleRateChange = MA_TRUE;
+
+ // Solution 2: Use conservative performance profile for larger buffers
+ config.performanceProfile = ma_performance_profile_conservative;
+
+ // Let Core Audio choose the period size based on conservative profile
+ config.periodSizeInFrames = 0; // 0 = let backend decide
+ config.periods = 0; // 0 = let backend decide based on performance profile
+
config.dataCallback = MiniaudioBackend::audio_callback;
config.pUserData = this;
@@ -49,6 +182,33 @@ void MiniaudioBackend::init() {
return;
}
+#if defined(DEBUG_LOG_AUDIO)
+ // Log actual device configuration (to stderr for visibility)
+ DEBUG_AUDIO("\n=== MINIAUDIO DEVICE CONFIGURATION ===\n");
+ DEBUG_AUDIO(" Sample rate: %u (requested: 32000)\n", device_.sampleRate);
+ DEBUG_AUDIO(" Channels: %u (requested: 2)\n", device_.playback.channels);
+ DEBUG_AUDIO(" Format: %d (requested: %d, f32=%d)\n",
+ device_.playback.format, config.playback.format, ma_format_f32);
+ DEBUG_AUDIO(" Period size: %u frames (%.1fms at %uHz)\n",
+ device_.playback.internalPeriodSizeInFrames,
+ (float)device_.playback.internalPeriodSizeInFrames / device_.sampleRate * 1000.0f,
+ device_.sampleRate);
+ DEBUG_AUDIO(" Periods: %u (buffer multiplier)\n", device_.playback.internalPeriods);
+ DEBUG_AUDIO(" Backend: %s\n", ma_get_backend_name(device_.pContext->backend));
+ DEBUG_AUDIO(" Total buffer size: %u frames (%.2fms) [period * periods]\n",
+ device_.playback.internalPeriodSizeInFrames * device_.playback.internalPeriods,
+ (float)(device_.playback.internalPeriodSizeInFrames * device_.playback.internalPeriods) / device_.sampleRate * 1000.0f);
+
+ // Calculate expected callback interval
+ if (device_.playback.internalPeriodSizeInFrames > 0) {
+ const float expected_callback_ms = (float)device_.playback.internalPeriodSizeInFrames / device_.sampleRate * 1000.0f;
+ DEBUG_AUDIO(" Expected callback interval: %.2fms (based on period size)\n", expected_callback_ms);
+ DEBUG_AUDIO(" WARNING: If actual callback interval differs, audio corruption may occur!\n");
+ }
+ DEBUG_AUDIO("======================================\n\n");
+ fflush(stderr);
+#endif /* defined(DEBUG_LOG_AUDIO) */
+
initialized_ = true;
}
diff --git a/src/audio/ring_buffer.cc b/src/audio/ring_buffer.cc
index 25cf853..ab51d6b 100644
--- a/src/audio/ring_buffer.cc
+++ b/src/audio/ring_buffer.cc
@@ -2,8 +2,11 @@
// It implements a lock-free ring buffer for audio streaming.
#include "ring_buffer.h"
+#include "util/debug.h"
#include <algorithm>
#include <cstring>
+#include <cstdlib> // for abort()
+#include <cstdio> // for fprintf()
AudioRingBuffer::AudioRingBuffer()
: capacity_(RING_BUFFER_CAPACITY_SAMPLES),
@@ -48,16 +51,42 @@ int AudioRingBuffer::write(const float* samples, int count) {
}
const int write = write_pos_.load(std::memory_order_acquire);
+
+ // BOUNDS CHECK: Validate write position
+ if (write < 0 || write >= capacity_) {
+ fprintf(stderr, "FATAL: write_pos out of bounds! write=%d, capacity=%d\n",
+ write, capacity_);
+ abort();
+ }
+
const int space_to_end = capacity_ - write;
if (to_write <= space_to_end) {
// Write in one chunk
+ // BOUNDS CHECK
+ if (write < 0 || write + to_write > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in write(): write=%d, to_write=%d, capacity=%d\n",
+ write, to_write, capacity_);
+ abort();
+ }
memcpy(&buffer_[write], samples, to_write * sizeof(float));
write_pos_.store((write + to_write) % capacity_, std::memory_order_release);
} else {
// Write in two chunks (wrap around)
+ // BOUNDS CHECK - first chunk
+ if (write < 0 || write + space_to_end > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in write() chunk1: write=%d, space_to_end=%d, capacity=%d\n",
+ write, space_to_end, capacity_);
+ abort();
+ }
memcpy(&buffer_[write], samples, space_to_end * sizeof(float));
const int remainder = to_write - space_to_end;
+ // BOUNDS CHECK - second chunk
+ if (remainder < 0 || remainder > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in write() chunk2: remainder=%d, capacity=%d\n",
+ remainder, capacity_);
+ abort();
+ }
memcpy(&buffer_[0], samples + space_to_end, remainder * sizeof(float));
write_pos_.store(remainder, std::memory_order_release);
}
@@ -71,16 +100,42 @@ int AudioRingBuffer::read(float* samples, int count) {
if (to_read > 0) {
const int read = read_pos_.load(std::memory_order_acquire);
+
+ // BOUNDS CHECK: Validate read position
+ if (read < 0 || read >= capacity_) {
+ fprintf(stderr, "FATAL: read_pos out of bounds! read=%d, capacity=%d\n",
+ read, capacity_);
+ abort();
+ }
+
const int space_to_end = capacity_ - read;
if (to_read <= space_to_end) {
// Read in one chunk
+ // BOUNDS CHECK
+ if (read < 0 || read + to_read > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in read(): read=%d, to_read=%d, capacity=%d\n",
+ read, to_read, capacity_);
+ abort();
+ }
memcpy(samples, &buffer_[read], to_read * sizeof(float));
read_pos_.store((read + to_read) % capacity_, std::memory_order_release);
} else {
// Read in two chunks (wrap around)
+ // BOUNDS CHECK - first chunk
+ if (read < 0 || read + space_to_end > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in read() chunk1: read=%d, space_to_end=%d, capacity=%d\n",
+ read, space_to_end, capacity_);
+ abort();
+ }
memcpy(samples, &buffer_[read], space_to_end * sizeof(float));
const int remainder = to_read - space_to_end;
+ // BOUNDS CHECK - second chunk
+ if (remainder < 0 || remainder > capacity_) {
+ fprintf(stderr, "BOUNDS ERROR in read() chunk2: remainder=%d, capacity=%d\n",
+ remainder, capacity_);
+ abort();
+ }
memcpy(samples + space_to_end, &buffer_[0], remainder * sizeof(float));
read_pos_.store(remainder, std::memory_order_release);
}
@@ -90,6 +145,14 @@ int AudioRingBuffer::read(float* samples, int count) {
// Fill remainder with silence if not enough samples available
if (to_read < count) {
+#if defined(DEBUG_LOG_RING_BUFFER)
+ // UNDERRUN DETECTED
+ static int underrun_count = 0;
+ if (++underrun_count % 10 == 1) { // Log every 10th underrun
+ DEBUG_RING_BUFFER("UNDERRUN #%d: requested=%d, available=%d, filling %d with silence\n",
+ underrun_count, count, to_read, count - to_read);
+ }
+#endif /* defined(DEBUG_LOG_RING_BUFFER) */
memset(samples + to_read, 0, (count - to_read) * sizeof(float));
}
diff --git a/src/audio/ring_buffer.h b/src/audio/ring_buffer.h
index d6c41ce..6375161 100644
--- a/src/audio/ring_buffer.h
+++ b/src/audio/ring_buffer.h
@@ -7,10 +7,11 @@
#include <atomic>
#include <cstdint>
-// Ring buffer capacity: 200ms @ 32kHz stereo
-// = 200ms * 32000 samples/sec * 2 channels / 1000ms = 12800 samples
-// This is exactly 25 DCT frames (25 * 512 = 12800)
-#define RING_BUFFER_LOOKAHEAD_MS 200
+// Ring buffer capacity: 400ms @ 32kHz stereo (increased for tempo scaling)
+// = 400ms * 32000 samples/sec * 2 channels / 1000ms = 25600 samples
+// This is exactly 50 DCT frames (50 * 512 = 25600)
+// At 2.0x tempo, we consume content 2x faster, so we need 2x buffer
+#define RING_BUFFER_LOOKAHEAD_MS 400
#define RING_BUFFER_SAMPLE_RATE 32000
#define RING_BUFFER_CHANNELS 2
#define RING_BUFFER_CAPACITY_SAMPLES \
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index 67bc46e..1afb501 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -5,15 +5,16 @@
#include "synth.h"
#include "audio/dct.h"
#include "audio/window.h"
+#include "util/debug.h"
#include <atomic>
#include <math.h>
#include <stdio.h> // For printf
#include <string.h> // For memset
-#if !defined(STRIP_ALL)
+#if defined(DEBUG_LOG_SYNTH)
#include "audio/audio.h"
#include "audio/audio_backend.h"
-#endif /* !defined(STRIP_ALL) */
+#endif /* defined(DEBUG_LOG_SYNTH) */
struct Voice {
bool active;
@@ -27,6 +28,7 @@ struct Voice {
float time_domain_buffer[DCT_SIZE];
int buffer_pos;
+ float fractional_pos; // Fractional sample position for tempo scaling
const volatile float* active_spectral_data;
};
@@ -41,18 +43,19 @@ static Voice g_voices[MAX_VOICES];
static volatile float g_current_output_peak =
0.0f; // Global peak for visualization
static float g_hamming_window[WINDOW_SIZE]; // Static window for optimization
+static float g_tempo_scale = 1.0f; // Playback speed multiplier
-#if !defined(STRIP_ALL)
+#if defined(DEBUG_LOG_SYNTH)
static float g_elapsed_time_sec = 0.0f; // Tracks elapsed time for event hooks
-#endif /* !defined(STRIP_ALL) */
+#endif /* defined(DEBUG_LOG_SYNTH) */
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)
+#if defined(DEBUG_LOG_SYNTH)
g_elapsed_time_sec = 0.0f;
-#endif /* !defined(STRIP_ALL) */
+#endif /* defined(DEBUG_LOG_SYNTH) */
// Initialize the Hamming window once
hamming_window_512(g_hamming_window);
}
@@ -61,7 +64,42 @@ void synth_shutdown() {
// Nothing to do here since we are not allocating memory
}
+void synth_set_tempo_scale(float tempo_scale) {
+ g_tempo_scale = tempo_scale;
+}
+
int synth_register_spectrogram(const Spectrogram* spec) {
+#if defined(DEBUG_LOG_SYNTH)
+ // VALIDATION: Check spectrogram pointer and data
+ if (spec == nullptr) {
+ DEBUG_SYNTH( "[SYNTH ERROR] Null spectrogram pointer\n");
+ return -1;
+ }
+ if (spec->spectral_data_a == nullptr || spec->spectral_data_b == nullptr) {
+ DEBUG_SYNTH( "[SYNTH ERROR] Null spectral data pointers\n");
+ return -1;
+ }
+ if (spec->num_frames <= 0 || spec->num_frames > 10000) {
+ DEBUG_SYNTH( "[SYNTH ERROR] Invalid num_frames=%d (must be 1-10000)\n",
+ spec->num_frames);
+ return -1;
+ }
+ // VALIDATION: Check spectral data isn't all zeros (common corruption symptom)
+ bool all_zero = true;
+ const float* data = spec->spectral_data_a;
+ const int samples_to_check = (spec->num_frames > 10) ? 10 * DCT_SIZE : spec->num_frames * DCT_SIZE;
+ for (int j = 0; j < samples_to_check; ++j) {
+ if (data[j] != 0.0f) {
+ all_zero = false;
+ break;
+ }
+ }
+ if (all_zero) {
+ DEBUG_SYNTH( "[SYNTH WARNING] Spectrogram appears to be all zeros (num_frames=%d)\n",
+ spec->num_frames);
+ }
+#endif
+
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
if (!g_synth_data.spectrogram_registered[i]) {
g_synth_data.spectrograms[i] = *spec;
@@ -118,9 +156,26 @@ void synth_commit_update(int spectrogram_id) {
void synth_trigger_voice(int spectrogram_id, float volume, float pan) {
if (spectrogram_id < 0 || spectrogram_id >= MAX_SPECTROGRAMS ||
!g_synth_data.spectrogram_registered[spectrogram_id]) {
+#if defined(DEBUG_LOG_SYNTH)
+ DEBUG_SYNTH( "[SYNTH ERROR] Invalid spectrogram_id=%d in trigger_voice\n",
+ spectrogram_id);
+#endif
return;
}
+#if defined(DEBUG_LOG_SYNTH)
+ // VALIDATION: Check volume and pan ranges
+ if (volume < 0.0f || volume > 2.0f) {
+ DEBUG_SYNTH( "[SYNTH WARNING] Unusual volume=%.2f for spectrogram_id=%d\n",
+ volume, spectrogram_id);
+ }
+ if (pan < -1.0f || pan > 1.0f) {
+ DEBUG_SYNTH( "[SYNTH WARNING] Invalid pan=%.2f (clamping) for spectrogram_id=%d\n",
+ pan, spectrogram_id);
+ pan = (pan < -1.0f) ? -1.0f : 1.0f;
+ }
+#endif
+
for (int i = 0; i < MAX_VOICES; ++i) {
if (!g_voices[i].active) {
Voice& v = g_voices[i];
@@ -136,17 +191,18 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan) {
v.total_spectral_frames =
g_synth_data.spectrograms[spectrogram_id].num_frames;
v.buffer_pos = DCT_SIZE; // Force IDCT on first render
+ v.fractional_pos = 0.0f; // Initialize fractional position for tempo scaling
v.active_spectral_data =
g_synth_data.active_spectrogram_data[spectrogram_id];
-#if !defined(STRIP_ALL)
+#if defined(DEBUG_LOG_SYNTH)
// 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) */
+#endif /* defined(DEBUG_LOG_SYNTH) */
return; // Voice triggered
}
@@ -199,6 +255,7 @@ void synth_render(float* output_buffer, int num_frames) {
left_sample += voice_sample * v.pan_left;
right_sample += voice_sample * v.pan_right;
+ // Advance voice position
++v.buffer_pos;
}
@@ -210,11 +267,11 @@ void synth_render(float* output_buffer, int num_frames) {
g_current_output_peak, fmaxf(fabsf(left_sample), fabsf(right_sample)));
}
-#if !defined(STRIP_ALL)
+#if defined(DEBUG_LOG_SYNTH)
// 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) */
+#endif /* defined(DEBUG_LOG_SYNTH) */
}
int synth_get_active_voice_count() {
diff --git a/src/audio/synth.h b/src/audio/synth.h
index a8f15a9..77b1878 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -9,9 +9,16 @@
// Based on tracker score analysis (see generated/music_data.cc)
// Max simultaneous patterns: 5, recommended: 10 each
-// Using 16 for comfortable headroom
-#define MAX_VOICES 16
-#define MAX_SPECTROGRAMS 16
+//
+// CACHING IMPLEMENTATION (COMPLETED):
+// - All asset samples are registered ONCE in tracker_init()
+// - All generated notes are cached by parameters (freq, duration, etc.)
+// - Current track: 14 unique samples (8 assets + 6 generated notes)
+// - With caching: MAX_SPECTROGRAMS = 32 provides 2.3x headroom
+//
+// Memory cost: 32 slots × 48 bytes = 1.5KB (down from 12KB with 256 slots)
+#define MAX_VOICES 48 // Per tracker_compiler: required=24, recommended=48
+#define MAX_SPECTROGRAMS 32 // Current track: 14 unique, 32 provides comfortable headroom
struct Spectrogram {
const float* spectral_data_a; // Front buffer
@@ -32,6 +39,7 @@ void synth_commit_update(int spectrogram_id);
void synth_trigger_voice(int spectrogram_id, float volume, float pan);
void synth_render(float* output_buffer, int num_frames);
+void synth_set_tempo_scale(float tempo_scale); // Set playback speed (1.0 = normal)
int synth_get_active_voice_count();
float synth_get_output_peak();
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index cb97f23..5358e50 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -2,6 +2,7 @@
#include "audio.h"
#include "audio/synth.h"
#include "util/asset_manager.h"
+#include "util/debug.h"
#include <cstring>
#include <vector>
@@ -27,6 +28,14 @@ struct ManagedSpectrogram {
static ManagedSpectrogram g_spec_pool[MAX_SPECTROGRAMS];
static int g_next_pool_slot = 0; // Round-robin allocation
+// CACHE: Pre-registered synth_ids for all samples (indexed by sample_id)
+// This eliminates redundant spectrogram generation and registration
+static int g_sample_synth_cache[256]; // Max 256 unique samples
+static bool g_cache_initialized = false;
+
+// Forward declarations
+static int get_free_pool_slot();
+
void tracker_init() {
g_last_trigger_idx = 0;
g_next_pool_slot = 0;
@@ -36,6 +45,78 @@ void tracker_init() {
g_spec_pool[i].active = false;
g_active_patterns[i].active = false;
}
+
+ // Initialize sample cache
+ if (!g_cache_initialized) {
+ for (int i = 0; i < 256; ++i) {
+ g_sample_synth_cache[i] = -1;
+ }
+
+ // Pre-register all unique samples (assets + generated notes)
+ for (uint32_t sid = 0; sid < g_tracker_samples_count; ++sid) {
+ AssetId aid = g_tracker_sample_assets[sid];
+
+ if (aid != AssetId::ASSET_LAST_ID) {
+ // ASSET sample: Load once and cache
+ size_t size;
+ const uint8_t* data = GetAsset(aid, &size);
+ if (data && size >= sizeof(SpecHeader)) {
+ const SpecHeader* header = (const SpecHeader*)data;
+ const int note_frames = header->num_frames;
+ const float* spectral_data = (const float*)(data + sizeof(SpecHeader));
+
+ Spectrogram spec;
+ spec.spectral_data_a = spectral_data;
+ spec.spectral_data_b = spectral_data;
+ spec.num_frames = note_frames;
+
+ g_sample_synth_cache[sid] = synth_register_spectrogram(&spec);
+
+#if defined(DEBUG_LOG_TRACKER)
+ if (g_sample_synth_cache[sid] == -1) {
+ DEBUG_TRACKER( "[TRACKER INIT] Failed to cache asset sample_id=%d (aid=%d)\n",
+ sid, (int)aid);
+ }
+#endif /* defined(DEBUG_LOG_TRACKER) */
+ }
+ } else {
+ // GENERATED note: Generate once and cache
+ const NoteParams& params = g_tracker_samples[sid];
+ int note_frames = 0;
+ std::vector<float> note_data = generate_note_spectrogram(params, &note_frames);
+
+ if (note_frames > 0) {
+ // Allocate persistent storage for this note
+ const int slot = get_free_pool_slot();
+ g_spec_pool[slot].data = new float[note_data.size()];
+ memcpy(g_spec_pool[slot].data, note_data.data(),
+ note_data.size() * sizeof(float));
+
+ Spectrogram spec;
+ spec.spectral_data_a = g_spec_pool[slot].data;
+ spec.spectral_data_b = g_spec_pool[slot].data;
+ spec.num_frames = note_frames;
+
+ g_sample_synth_cache[sid] = synth_register_spectrogram(&spec);
+ g_spec_pool[slot].synth_id = g_sample_synth_cache[sid];
+ g_spec_pool[slot].active = true; // Mark as permanently allocated
+
+#if defined(DEBUG_LOG_TRACKER)
+ if (g_sample_synth_cache[sid] == -1) {
+ DEBUG_TRACKER( "[TRACKER INIT] Failed to cache generated sample_id=%d (freq=%.2f)\n",
+ sid, params.base_freq);
+ }
+#endif /* defined(DEBUG_LOG_TRACKER) */
+ }
+ }
+ }
+
+ g_cache_initialized = true;
+
+#if defined(DEBUG_LOG_TRACKER)
+ DEBUG_TRACKER( "[TRACKER INIT] Cached %d unique samples\n", g_tracker_samples_count);
+#endif /* defined(DEBUG_LOG_TRACKER) */
+ }
}
void tracker_reset() {
@@ -67,57 +148,39 @@ static int get_free_pattern_slot() {
return -1; // No free slots
}
-// Helper to trigger a single note event
+// Helper to trigger a single note event (OPTIMIZED with caching)
static void trigger_note_event(const TrackerEvent& event) {
- std::vector<float> note_data;
- int note_frames = 0;
-
- // Load or generate the note spectrogram
- AssetId aid = g_tracker_sample_assets[event.sample_id];
- if (aid != AssetId::ASSET_LAST_ID) {
- size_t size;
- const uint8_t* data = GetAsset(aid, &size);
- if (data && size >= sizeof(SpecHeader)) {
- const SpecHeader* header = (const SpecHeader*)data;
- note_frames = header->num_frames;
- const float* src_spectral_data =
- (const float*)(data + sizeof(SpecHeader));
- note_data.assign(src_spectral_data,
- src_spectral_data + (size_t)note_frames * DCT_SIZE);
- }
- } else {
- const NoteParams& params = g_tracker_samples[event.sample_id];
- note_data = generate_note_spectrogram(params, &note_frames);
+#if defined(DEBUG_LOG_TRACKER)
+ // VALIDATION: Check sample_id bounds
+ if (event.sample_id >= g_tracker_samples_count) {
+ DEBUG_TRACKER( "[TRACKER ERROR] Invalid sample_id=%d (max=%d)\n",
+ event.sample_id, g_tracker_samples_count - 1);
+ return;
}
+ // VALIDATION: Check volume and pan ranges
+ if (event.volume < 0.0f || event.volume > 2.0f) {
+ DEBUG_TRACKER( "[TRACKER WARNING] Unusual volume=%.2f for sample_id=%d\n",
+ event.volume, event.sample_id);
+ }
+ if (event.pan < -1.0f || event.pan > 1.0f) {
+ DEBUG_TRACKER( "[TRACKER WARNING] Invalid pan=%.2f for sample_id=%d\n",
+ event.pan, event.sample_id);
+ }
+#endif /* defined(DEBUG_LOG_TRACKER) */
- if (note_frames > 0) {
- const int slot = get_free_pool_slot();
-
- // Clean up old data in this slot if reusing
- if (g_spec_pool[slot].synth_id != -1) {
- synth_unregister_spectrogram(g_spec_pool[slot].synth_id);
- g_spec_pool[slot].synth_id = -1;
- }
- if (g_spec_pool[slot].data != nullptr) {
- delete[] g_spec_pool[slot].data;
- g_spec_pool[slot].data = nullptr;
- }
-
- // Allocate and register new note
- g_spec_pool[slot].data = new float[note_data.size()];
- memcpy(g_spec_pool[slot].data, note_data.data(),
- note_data.size() * sizeof(float));
-
- Spectrogram spec;
- spec.spectral_data_a = g_spec_pool[slot].data;
- spec.spectral_data_b = g_spec_pool[slot].data;
- spec.num_frames = note_frames;
-
- g_spec_pool[slot].synth_id = synth_register_spectrogram(&spec);
- g_spec_pool[slot].active = true;
+ // OPTIMIZED: Use cached synth_id instead of regenerating spectrogram
+ const int cached_synth_id = g_sample_synth_cache[event.sample_id];
- synth_trigger_voice(g_spec_pool[slot].synth_id, event.volume, event.pan);
+#if defined(DEBUG_LOG_TRACKER)
+ if (cached_synth_id == -1) {
+ DEBUG_TRACKER( "[TRACKER ERROR] No cached synth_id for sample_id=%d (init failed?)\n",
+ event.sample_id);
+ return;
}
+#endif /* defined(DEBUG_LOG_TRACKER) */
+
+ // Trigger voice directly with cached spectrogram
+ synth_trigger_voice(cached_synth_id, event.volume, event.pan);
}
void tracker_update(float music_time_sec) {