summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-28 18:45:42 +0100
committerskal <pascal.massimino@gmail.com>2026-03-28 20:14:59 +0100
commit8bee7577cba9f55be8bc404038d5df959595b989 (patch)
tree4a228f02a3345d9e1275b9e0949d2fb5d6d1864a /src
parent37df61d1a0dbd5e253f9db778c17c4187e453b8d (diff)
fix(audio): fix early timing drift in tracker, use ola_decode_frame in synth
- Replaced chunk_frames truncation accumulation with accurate double-precision integration in audio_render_ahead. - Updated tracker to use double-precision time representations for exact sample-accurate scheduling. - Extracted ola_decode_frame to handle per-frame OLA-IDCT synthesis in synth.cc. - Updated TODO.md for completed audio tasks. handoff(Claude): Audio timing drift and OLA-IDCT enhancement resolved.
Diffstat (limited to 'src')
-rw-r--r--src/audio/audio_engine.cc4
-rw-r--r--src/audio/audio_engine.h2
-rw-r--r--src/audio/ola.cc16
-rw-r--r--src/audio/ola.h6
-rw-r--r--src/audio/synth.cc16
-rw-r--r--src/audio/tracker.cc38
-rw-r--r--src/audio/tracker.h2
7 files changed, 41 insertions, 43 deletions
diff --git a/src/audio/audio_engine.cc b/src/audio/audio_engine.cc
index 8e47eac..b4c4863 100644
--- a/src/audio/audio_engine.cc
+++ b/src/audio/audio_engine.cc
@@ -81,8 +81,8 @@ void AudioEngine::load_music_data(const TrackerScore* score,
#endif
}
-void AudioEngine::update(float music_time, float dt) {
- current_time_ = music_time;
+void AudioEngine::update(double music_time, double dt) {
+ current_time_ = (float)music_time;
// Pre-warm samples needed in next 2 seconds (lazy loading strategy)
// TODO: Implement pre-warming based on upcoming pattern triggers
diff --git a/src/audio/audio_engine.h b/src/audio/audio_engine.h
index cc4c80a..c6fe4db 100644
--- a/src/audio/audio_engine.h
+++ b/src/audio/audio_engine.h
@@ -21,7 +21,7 @@ class AudioEngine {
const AssetId* sample_assets, uint32_t sample_count);
// Update loop
- void update(float music_time, float dt);
+ void update(double music_time, double dt);
#if !defined(STRIP_ALL)
// Timeline seeking (debugging only)
diff --git a/src/audio/ola.cc b/src/audio/ola.cc
index 738df85..fd5098e 100644
--- a/src/audio/ola.cc
+++ b/src/audio/ola.cc
@@ -21,14 +21,18 @@ void ola_encode(const float* pcm, int n_samples, float* spec, int num_frames) {
}
}
+void ola_decode_frame(const float* spec_frame, float* overlap, float* out_hop) {
+ float tmp[DCT_SIZE];
+ idct_512(spec_frame, tmp);
+ for (int j = 0; j < OLA_HOP_SIZE; ++j)
+ out_hop[j] = tmp[j] + overlap[j];
+ for (int j = 0; j < OLA_OVERLAP; ++j)
+ overlap[j] = tmp[OLA_HOP_SIZE + j];
+}
+
void ola_decode(const float* spec, int num_frames, float* pcm) {
float overlap[OLA_OVERLAP] = {};
- float tmp[DCT_SIZE];
for (int f = 0; f < num_frames; ++f) {
- idct_512(spec + f * DCT_SIZE, tmp);
- for (int j = 0; j < OLA_HOP_SIZE; ++j)
- pcm[f * OLA_HOP_SIZE + j] = tmp[j] + overlap[j];
- for (int j = 0; j < OLA_OVERLAP; ++j)
- overlap[j] = tmp[OLA_HOP_SIZE + j];
+ ola_decode_frame(spec + f * DCT_SIZE, overlap, pcm + f * OLA_HOP_SIZE);
}
}
diff --git a/src/audio/ola.h b/src/audio/ola.h
index 2d6267e..1fb2a4a 100644
--- a/src/audio/ola.h
+++ b/src/audio/ola.h
@@ -18,3 +18,9 @@ void ola_encode(const float* pcm, int n_samples, float* spec, int num_frames);
// Hann at 50% overlap satisfies w[n]+w[n+H]=1 → perfect reconstruction.
// pcm must hold num_frames * OLA_HOP_SIZE floats.
void ola_decode(const float* spec, int num_frames, float* pcm);
+
+// Single-frame OLA-IDCT decoder.
+// spec_frame: single DCT_SIZE spectral frame.
+// overlap: OLA_OVERLAP buffer (read/write). Must be zero-initialized for first frame.
+// out_hop: OLA_HOP_SIZE buffer for the resulting time-domain samples.
+void ola_decode_frame(const float* spec_frame, float* overlap, float* out_hop);
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index 45ced59..9b56069 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -4,6 +4,7 @@
#include "synth.h"
#include "audio/dct.h"
+#include "audio/ola.h"
#include "util/debug.h"
#include <math.h>
#include <string.h> // For memset
@@ -264,20 +265,7 @@ void synth_render(float* output_buffer, int num_frames) {
(v.current_spectral_frame * DCT_SIZE);
if (v.ola_mode) {
- // OLA-IDCT synthesis (v2): no synthesis window.
- // Analysis used Hann; at 50% overlap w[n]+w[n+H]=1 so
- // rectangular synthesis gives perfect reconstruction.
- float tmp[DCT_SIZE];
- idct_512(spectral_frame, tmp);
- // Add saved overlap from previous frame
- for (int j = 0; j < OLA_OVERLAP; ++j)
- tmp[j] += v.overlap_buf[j];
- // Save new tail as overlap for next frame
- for (int j = 0; j < OLA_OVERLAP; ++j)
- v.overlap_buf[j] = tmp[OLA_HOP_SIZE + j];
- // Output buffer holds first OLA_HOP_SIZE samples
- for (int j = 0; j < OLA_HOP_SIZE; ++j)
- v.time_domain_buffer[j] = tmp[j];
+ ola_decode_frame(spectral_frame, v.overlap_buf, v.time_domain_buffer);
} else {
// V1: IDCT directly, no windowing
idct_512(spectral_frame, v.time_domain_buffer);
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 651c4d2..95b8022 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -20,7 +20,7 @@ static uint32_t g_last_trigger_idx = 0;
// Active pattern instance tracking
struct ActivePattern {
uint16_t pattern_id;
- float start_music_time; // When this pattern was triggered (music time)
+ double start_music_time; // When this pattern was triggered (music time)
uint32_t next_event_idx; // Next event to trigger within this pattern
bool active;
};
@@ -315,21 +315,21 @@ static void trigger_note_event(const TrackerEvent& event,
start_offset_samples);
}
-void tracker_update(float music_time_sec, float dt_music_sec) {
+void tracker_update(double music_time_sec, double dt_music_sec) {
// Unit-less timing: 1 unit = 4 beats (by convention)
- const float BEATS_PER_UNIT = 4.0f;
- const float unit_duration_sec =
- (BEATS_PER_UNIT / g_tracker_score.bpm) * 60.0f;
+ const double BEATS_PER_UNIT = 4.0;
+ const double unit_duration_sec =
+ (BEATS_PER_UNIT / (double)g_tracker_score.bpm) * 60.0;
- const float end_music_time = music_time_sec + dt_music_sec;
- const float tempo_scale = synth_get_tempo_scale();
+ const double end_music_time = music_time_sec + dt_music_sec;
+ const double tempo_scale = (double)synth_get_tempo_scale();
// Step 1: Process new pattern triggers
while (g_last_trigger_idx < g_tracker_score.num_triggers) {
const TrackerPatternTrigger& trigger =
g_tracker_score.triggers[g_last_trigger_idx];
- const float trigger_time_sec = trigger.unit_time * unit_duration_sec;
+ const double trigger_time_sec = (double)trigger.unit_time * unit_duration_sec;
if (trigger_time_sec > end_music_time)
break;
@@ -357,8 +357,8 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
// Trigger all events that have passed their unit time
while (active.next_event_idx < pattern.num_events) {
const TrackerEvent& event = pattern.events[active.next_event_idx];
- const float event_music_time =
- active.start_music_time + event.unit_time * unit_duration_sec;
+ const double event_music_time =
+ active.start_music_time + (double)event.unit_time * unit_duration_sec;
if (event_music_time > end_music_time)
break; // This event hasn't reached its time yet
@@ -368,7 +368,7 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
int sample_offset = 0;
if (event_music_time > music_time_sec) {
sample_offset = (int)((event_music_time - music_time_sec) / tempo_scale *
- RING_BUFFER_SAMPLE_RATE);
+ (double)RING_BUFFER_SAMPLE_RATE);
}
// Apply humanization if enabled
@@ -378,22 +378,22 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
uint32_t event_hash = g_tracker_score.humanize_seed ^
(active.pattern_id << 16) ^ active.next_event_idx;
std::minstd_rand rng(event_hash);
- std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
+ std::uniform_real_distribution<double> dist(-1.0, 1.0);
// Timing variation: jitter by % of beat duration
if (g_tracker_score.timing_variation_pct > 0.0f) {
- float beat_sec = 60.0f / g_tracker_score.bpm;
- float jitter = dist(rng) *
- (g_tracker_score.timing_variation_pct / 100.0f) *
+ double beat_sec = 60.0 / (double)g_tracker_score.bpm;
+ double jitter = dist(rng) *
+ (double)(g_tracker_score.timing_variation_pct / 100.0f) *
beat_sec;
sample_offset +=
- (int)(jitter / tempo_scale * RING_BUFFER_SAMPLE_RATE);
+ (int)(jitter / tempo_scale * (double)RING_BUFFER_SAMPLE_RATE);
}
// Volume variation: vary by %
if (g_tracker_score.volume_variation_pct > 0.0f) {
volume_mult +=
- dist(rng) * (g_tracker_score.volume_variation_pct / 100.0f);
+ (float)(dist(rng) * (double)(g_tracker_score.volume_variation_pct / 100.0f));
}
}
@@ -402,8 +402,8 @@ void tracker_update(float music_time_sec, float dt_music_sec) {
}
// Pattern remains active until full duration elapses
- const float pattern_end_time =
- active.start_music_time + pattern.unit_length * unit_duration_sec;
+ const double pattern_end_time =
+ active.start_music_time + (double)pattern.unit_length * unit_duration_sec;
if (pattern_end_time <= end_music_time) {
active.active = false;
}
diff --git a/src/audio/tracker.h b/src/audio/tracker.h
index 8e89a4e..f471bd8 100644
--- a/src/audio/tracker.h
+++ b/src/audio/tracker.h
@@ -45,5 +45,5 @@ extern const uint32_t g_tracker_patterns_count;
extern const TrackerScore g_tracker_score;
void tracker_init();
-void tracker_update(float music_time_sec, float dt_music_sec);
+void tracker_update(double music_time_sec, double dt_music_sec);
void tracker_reset(); // Reset tracker state (for tests/seeking)