summaryrefslogtreecommitdiff
path: root/src/audio/synth.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-02 09:38:46 +0100
committerskal <pascal.massimino@gmail.com>2026-03-02 09:38:46 +0100
commitbb8197075161f9c9ded51beab913150b43954e2c (patch)
treec3835a0cb2133be28e5cc695b487395250fd1ea0 /src/audio/synth.cc
parentb2ca699723b1695bd712ec68635a947c6ca3fd42 (diff)
feat(audio): OLA-IDCT synthesis with Hann window to eliminate clicks
Add v2 spectrogram format (SPEC_VERSION_V2_OLA) using overlap-add IDCT with 50% overlap (hop=256, OLA_OVERLAP=256) and Hann windowing. - dct.h: OLA_HOP_SIZE=256, OLA_OVERLAP=256 - synth.h: SPEC_VERSION_V1/V2_OLA constants; version field on Spectrogram - window.h/cc: hann_window_512() alongside existing hamming_window_512() - synth.cc: g_hann[] precomputed at init; OLA path in synth_render when ola_mode=true (IDCT -> Hann -> add overlap tail -> save new tail -> output OLA_HOP_SIZE samples); v1 path unchanged for backward compat - tracker.cc: MP3 encoder now uses sliding 512-sample Hann window with OLA_HOP_SIZE advance per frame; sets version=SPEC_VERSION_V2_OLA; .spec files propagate header->version; generated notes stay v1 Existing .spec files must be regenerated to benefit from click-free OLA. handoff(Claude): OLA done. .spec files need regen via MP3 tool to activate v2.
Diffstat (limited to 'src/audio/synth.cc')
-rw-r--r--src/audio/synth.cc35
1 files changed, 30 insertions, 5 deletions
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index 5fadf3c..a723404 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -27,6 +27,8 @@ struct Voice {
int total_spectral_frames;
float time_domain_buffer[DCT_SIZE];
+ float overlap_buf[OLA_OVERLAP]; // OLA tail from previous frame (v2 only)
+ bool ola_mode; // True for SPEC_VERSION_V2_OLA
int buffer_pos;
float fractional_pos; // Fractional sample position for tempo scaling
@@ -45,6 +47,7 @@ static Voice g_voices[MAX_VOICES];
static volatile float g_current_output_peak =
0.0f; // Global peak for visualization
static float g_tempo_scale = 1.0f; // Playback speed multiplier
+static float g_hann[DCT_SIZE]; // Hann window for OLA synthesis (v2)
#if !defined(STRIP_ALL)
static float g_elapsed_time_sec = 0.0f; // Tracks elapsed time for event hooks
@@ -54,6 +57,7 @@ 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;
+ hann_window_512(g_hann);
#if !defined(STRIP_ALL)
g_elapsed_time_sec = 0.0f;
#endif /* !defined(STRIP_ALL) */
@@ -202,7 +206,11 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan,
v.current_spectral_frame = 0;
v.total_spectral_frames =
g_synth_data.spectrograms[spectrogram_id].num_frames;
- v.buffer_pos = DCT_SIZE; // Force IDCT on first render
+ v.ola_mode = (g_synth_data.spectrograms[spectrogram_id].version ==
+ SPEC_VERSION_V2_OLA);
+ v.buffer_pos = v.ola_mode ? OLA_HOP_SIZE : DCT_SIZE; // Force reload on first render
+ if (v.ola_mode)
+ memset(v.overlap_buf, 0, sizeof(v.overlap_buf));
v.fractional_pos =
0.0f; // Initialize fractional position for tempo scaling
v.start_sample_offset =
@@ -243,7 +251,8 @@ void synth_render(float* output_buffer, int num_frames) {
continue; // Don't produce audio until offset elapsed
}
- if (v.buffer_pos >= DCT_SIZE) {
+ const int frame_threshold = v.ola_mode ? OLA_HOP_SIZE : DCT_SIZE;
+ if (v.buffer_pos >= frame_threshold) {
if (v.current_spectral_frame >= v.total_spectral_frames) {
v.active = false;
continue;
@@ -256,9 +265,25 @@ void synth_render(float* output_buffer, int num_frames) {
const float* spectral_frame = (const float*)v.active_spectral_data +
(v.current_spectral_frame * DCT_SIZE);
- // IDCT directly - no windowing needed for synthesis
- // (Window is only used during analysis, before DCT)
- idct_512(spectral_frame, v.time_domain_buffer);
+ if (v.ola_mode) {
+ // OLA-IDCT synthesis (v2): Hann window + overlap-add
+ float tmp[DCT_SIZE];
+ idct_512(spectral_frame, tmp);
+ for (int j = 0; j < DCT_SIZE; ++j)
+ tmp[j] *= g_hann[j];
+ // 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];
+ } else {
+ // V1: IDCT directly, no windowing
+ idct_512(spectral_frame, v.time_domain_buffer);
+ }
v.buffer_pos = 0;
++v.current_spectral_frame;