From db6fbf8b8eae8b96d129ac673cbf11d67926996a Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Mar 2026 22:49:48 +0100 Subject: fix(audio): correct OLA synthesis and extract shared ola_encode/ola_decode - Remove erroneous Hann synthesis window from synth.cc (g_hann * tmp[j]). Hann analysis at 50% overlap satisfies w[n]+w[n+H]=1, so rectangular synthesis gives perfect reconstruction; applying Hann twice was wrong. - Extract ola_encode()/ola_decode()/ola_num_frames() into src/audio/ola.h+cc. spectool and test_wav_roundtrip now use the shared functions. synth.cc lazy-decode path stays inlined (see TODO for future refactor). - Drop dead include and g_hann array from synth.cc. - Drop dead window.h include from spectool.cc. - Update PROJECT_CONTEXT.md, COMPLETED.md, TODO.md to reflect correct analysis-only Hann window and new ola.h API. handoff(Gemini): OLA synthesis bug fixed + ola.h factorized. synth.cc lazy-decode still inline (TODO item added). 34/35 tests pass; WavDumpBackendTest failure is pre-existing and unrelated. Co-Authored-By: Claude Sonnet 4.6 --- src/audio/ola.cc | 34 ++++++++++++++++++++++++++++++++++ src/audio/ola.h | 21 +++++++++++++++++++++ src/audio/synth.cc | 10 +++------- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/audio/ola.cc create mode 100644 src/audio/ola.h (limited to 'src/audio') diff --git a/src/audio/ola.cc b/src/audio/ola.cc new file mode 100644 index 0000000..738df85 --- /dev/null +++ b/src/audio/ola.cc @@ -0,0 +1,34 @@ +// This file is part of the 64k demo project. +// Implements batch OLA encode/decode shared by spectool and tests. +// See ola.h for API documentation. + +#include "audio/ola.h" +#include "audio/window.h" +#include + +void ola_encode(const float* pcm, int n_samples, float* spec, int num_frames) { + float win[DCT_SIZE]; + hann_window_512(win); + float chunk[DCT_SIZE]; + for (int f = 0; f < num_frames; ++f) { + const int start = f * OLA_HOP_SIZE; + const int avail = + (start + DCT_SIZE <= n_samples) ? DCT_SIZE : n_samples - start; + for (int i = 0; i < avail; ++i) + chunk[i] = pcm[start + i] * win[i]; + memset(chunk + avail, 0, (DCT_SIZE - avail) * sizeof(float)); + fdct_512(chunk, spec + f * DCT_SIZE); + } +} + +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]; + } +} diff --git a/src/audio/ola.h b/src/audio/ola.h new file mode 100644 index 0000000..3dbc368 --- /dev/null +++ b/src/audio/ola.h @@ -0,0 +1,21 @@ +// This file is part of the 64k demo project. +// Shared OLA encode/decode helpers (Hann analysis, rectangular synthesis). +// Used by spectool, tests, and any batch PCM<->spec conversion. + +#pragma once +#include "audio/dct.h" + +// Returns number of OLA frames for n_samples PCM input. +static inline int ola_num_frames(int n_samples) { + return (n_samples > DCT_SIZE) ? (n_samples - DCT_SIZE) / OLA_HOP_SIZE + 1 + : 1; +} + +// Hann-windowed FDCT with 50% overlap (analysis). +// spec must hold ola_num_frames(n_samples) * DCT_SIZE floats. +void ola_encode(const float* pcm, int n_samples, float* spec, int num_frames); + +// IDCT-OLA with rectangular synthesis window (no synthesis window). +// 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); diff --git a/src/audio/synth.cc b/src/audio/synth.cc index a723404..3212e0b 100644 --- a/src/audio/synth.cc +++ b/src/audio/synth.cc @@ -4,9 +4,7 @@ #include "synth.h" #include "audio/dct.h" -#include "audio/window.h" #include "util/debug.h" -#include #include #include // For printf #include // For memset @@ -47,7 +45,6 @@ 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 @@ -57,7 +54,6 @@ 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) */ @@ -266,11 +262,11 @@ void synth_render(float* output_buffer, int num_frames) { (v.current_spectral_frame * DCT_SIZE); if (v.ola_mode) { - // OLA-IDCT synthesis (v2): Hann window + overlap-add + // 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); - 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]; -- cgit v1.2.3