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 ++---- src/tests/audio/test_wav_roundtrip.cc | 60 ++++++----------------------------- 4 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 src/audio/ola.cc create mode 100644 src/audio/ola.h (limited to 'src') 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]; diff --git a/src/tests/audio/test_wav_roundtrip.cc b/src/tests/audio/test_wav_roundtrip.cc index 6294d6d..79de6ad 100644 --- a/src/tests/audio/test_wav_roundtrip.cc +++ b/src/tests/audio/test_wav_roundtrip.cc @@ -1,9 +1,8 @@ // Tests the wav->spec->wav roundtrip SNR. -// Generates a sine wave, runs OLA-DCT analysis then IMDCT-OLA synthesis, +// Generates a sine wave, runs OLA encode then OLA decode, // and asserts the reconstruction SNR exceeds the threshold. -#include "audio/dct.h" -#include "audio/window.h" +#include "audio/ola.h" #include #include #include @@ -12,49 +11,6 @@ static const int SAMPLE_RATE = 32000; static const float PI = 3.14159265358979323846f; -// Replicate analyze_audio OLA pass (Hann + FDCT, hop = OLA_HOP_SIZE) -static std::vector ola_analyze(const std::vector& pcm) { - float win[DCT_SIZE]; - hann_window_512(win); - - const int hop = OLA_HOP_SIZE; - const int n_pcm = (int)pcm.size(); - const int num_frames = (n_pcm > DCT_SIZE) ? (n_pcm - DCT_SIZE) / hop + 1 : 1; - - std::vector spec(num_frames * DCT_SIZE); - float chunk[DCT_SIZE]; - - for (int f = 0; f < num_frames; ++f) { - const int start = f * hop; - const int avail = (start + DCT_SIZE <= n_pcm) ? DCT_SIZE : n_pcm - start; - for (int i = 0; i < avail; ++i) chunk[i] = pcm[start + i] * win[i]; - for (int i = avail; i < DCT_SIZE; ++i) chunk[i] = 0.0f; - - fdct_512(chunk, spec.data() + f * DCT_SIZE); - } - return spec; -} - -// IDCT + OLA synthesis (no synthesis window) matching decode_to_wav. -// Analysis used Hann; since Hann satisfies w[n]+w[n+H]=1 at 50% overlap, -// skipping the synthesis window gives perfect reconstruction. -static std::vector ola_decode(const std::vector& spec, - int num_frames) { - std::vector pcm(num_frames * OLA_HOP_SIZE + OLA_OVERLAP, 0.0f); - float overlap[OLA_OVERLAP] = {}; - float tmp[DCT_SIZE]; - - for (int f = 0; f < num_frames; ++f) { - idct_512(spec.data() + 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]; - } - pcm.resize(num_frames * OLA_HOP_SIZE); - return pcm; -} - static float compute_snr_db(const std::vector& ref, const std::vector& out, int skip_samples) { @@ -78,12 +34,14 @@ int main() { for (int i = 0; i < n_samples; ++i) input[i] = 0.5f * sinf(2.0f * PI * 440.0f * i / SAMPLE_RATE); - // Analyze - std::vector spec = ola_analyze(input); - const int num_frames = (int)(spec.size() / DCT_SIZE); + // Encode + const int num_frames = ola_num_frames(n_samples); + std::vector spec(num_frames * DCT_SIZE); + ola_encode(input.data(), n_samples, spec.data(), num_frames); - // Decode with IDCT-OLA (no synthesis window) - std::vector output = ola_decode(spec, num_frames); + // Decode + std::vector output(num_frames * OLA_HOP_SIZE); + ola_decode(spec.data(), num_frames, output.data()); // SNR — skip first DCT_SIZE samples (ramp-up transient) const float snr = compute_snr_db(input, output, DCT_SIZE); -- cgit v1.2.3