summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-05 22:49:48 +0100
committerskal <pascal.massimino@gmail.com>2026-03-05 22:49:48 +0100
commitdb6fbf8b8eae8b96d129ac673cbf11d67926996a (patch)
tree97d28da56a77c17f5583a9342a77cb065b25f31f /src
parentb7fc4aa9a6bd15ce9780d46a425971d523c10b92 (diff)
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 <atomic> 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 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/audio/ola.cc34
-rw-r--r--src/audio/ola.h21
-rw-r--r--src/audio/synth.cc10
-rw-r--r--src/tests/audio/test_wav_roundtrip.cc60
4 files changed, 67 insertions, 58 deletions
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 <string.h>
+
+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 <atomic>
#include <math.h>
#include <stdio.h> // For printf
#include <string.h> // 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 <assert.h>
#include <cmath>
#include <cstdio>
@@ -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<float> ola_analyze(const std::vector<float>& 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<float> 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<float> ola_decode(const std::vector<float>& spec,
- int num_frames) {
- std::vector<float> 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<float>& ref,
const std::vector<float>& 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<float> 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<float> spec(num_frames * DCT_SIZE);
+ ola_encode(input.data(), n_samples, spec.data(), num_frames);
- // Decode with IDCT-OLA (no synthesis window)
- std::vector<float> output = ola_decode(spec, num_frames);
+ // Decode
+ std::vector<float> 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);