OXYGENHUB
◆ main openmsx / src / utils / autocorrelation.hh
4.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef AUTOCORRELATION_HH
2
#define AUTOCORRELATION_HH
3
4
#include "FFTReal.hh"
5
6
#include <algorithm>
7
#include <array>
8
#include <cassert>
9
#include <span>
10
11
// Compute autocorrelation of a real signal:
12
//     autocorr[τ] = Σ signal[n] · signal[n+τ]
13
//
14
// Template parameters:
15
//   SIGNAL_LEN_L2 = log2(maximum allowed signal length).
16
//     This is a hard upper bound: callers must ensure `signal.size() <= pow(2, SIGNAL_LEN_L2)`.
17
//     For efficiency pick the smallest value that fits your input, i.e.
18
//     `SIGNAL_LEN_L2 = ceil(log2(signal.size()))`.
19
//   NORMALIZE = whether to normalize result by 1/FFT_LEN (default: true)
20
//
21
// Parameters:
22
//   signal: Input signal (length <= pow(2, SIGNAL_LEN_L2))
23
//   output: Autocorrelation result (same length as signal)
24
//
25
// Output properties:
26
//   output[0] = maximum value (signal energy)
27
//   output[τ] = autocorrelation at lag τ
28
//
29
// Algorithm (Wiener-Khinchin theorem):
30
//   1. Zero-pad signal to 2*signal_len for linear (not circular) correlation
31
//   2. Forward Real FFT: signal → spectrum (complex, Hermitian symmetric)
32
//   3. Power spectrum: power[k] = |spectrum[k]|² (real, even symmetric)
33
//   4. Inverse Real FFT: power → autocorrelation (via FFT of even-symmetric input)
34
template<unsigned SIGNAL_LEN_L2, bool NORMALIZE = true>
35
void autocorrelation(std::span<const float> signal, std::span<float> output)
36
{
37
	static_assert(SIGNAL_LEN_L2 >= 2);  // max signal length at least 4
38
	static_assert(SIGNAL_LEN_L2 <= 15); // max signal length at most 32768
39
	static constexpr unsigned SIGNAL_LEN = 1 << SIGNAL_LEN_L2; // Maximum signal length
40
	static constexpr unsigned FFT_LEN = 2 * SIGNAL_LEN; // For linear correlation padding
41
42
	const auto N = signal.size();
43
	assert(N <= SIGNAL_LEN);
44
	assert(N == output.size());
45
46
	// Step 1: Zero-pad signal to FFT_LEN. This serves two purposes:
47
	// 1. Extend from N to 2*N (required for linear autocorrelation, not circular)
48
	// 2. Round up to power-of-2 (required by FFT)
49
	std::array<float, FFT_LEN> paddedSignal;  // Zero-padded input signal
50
	std::ranges::copy(signal, paddedSignal.begin());
51
	std::ranges::fill(std::span(paddedSignal).subspan(N), 0.0f);
52
53
	// Step 2: Forward Real FFT
54
	// paddedSignal (time) → spectrum (frequency)
55
	std::array<float, FFT_LEN> spectrum; // FFT spectrum / power spectrum
56
	std::array<float, FFT_LEN> fftWork;  // Temporary work buffer for FFT operations
57
	FFTReal<SIGNAL_LEN_L2 + 1>::execute(paddedSignal, spectrum, fftWork);
58
	// spectrum now contains FFT in packed format:
59
	//   [Re[0], Re[1], ..., Re[N/2], Im[1], ..., Im[N/2-1]]
60
61
	// Step 3: Compute power spectrum: |FFT|² = Re² + Im² (in-place)
62
	// DC component (k=0): purely real, no imaginary part
63
	spectrum[0] = spectrum[0] * spectrum[0];
64
	// Nyquist component (k=FFT_LEN/2): purely real, no imaginary part
65
	spectrum[SIGNAL_LEN] = spectrum[SIGNAL_LEN] * spectrum[SIGNAL_LEN];
66
	// Other bins (k=1 to SIGNAL_LEN-1): complex values
67
	// Real parts: spectrum[1..SIGNAL_LEN-1]
68
	// Imaginary parts: spectrum[SIGNAL_LEN+1..FFT_LEN-1]
69
	for (unsigned i = 1; i < SIGNAL_LEN; ++i) {
70
		const float re = spectrum[i];
71
		const float im = spectrum[SIGNAL_LEN + i];
72
		spectrum[i] = re * re + im * im; // |spectrum[i]|²
73
	}
74
	// spectrum now contains: [P[0], P[1], ..., P[N/2], <stuff that will be overwritten>]
75
	// where P[k] = |spectrum[k]|² (real-valued power spectrum)
76
77
	// Step 3.1: Mirror the power spectrum to create even symmetry
78
	// We need: [P[0], P[1], ..., P[SIGNAL_LEN], P[SIGNAL_LEN-1], ..., P[1]]
79
	// using even symmetry: P[-k] = P[k] = P[N-k]
80
	for (unsigned i = 1; i < SIGNAL_LEN; ++i) {
81
		spectrum[FFT_LEN - i] = spectrum[i]; // P[N-k] = P[k]
82
	}
83
	// spectrum is now: a complete real + even symmetric signal
84
85
	// Step 4: Apply IFFT to power spectrum
86
	//
87
	// General IFFT formula: IFFT(X) = conj(FFT(conj(X))) / N
88
	//
89
	// In our case:
90
	// - Input X (power spectrum) is real: conj(X) = X
91
	// - Input X is even symmetric (after Step 3.1)
92
	// - FFT of real+even symmetric signal yields real+even symmetric output
93
	// - Therefore: conj(FFT(X)) = FFT(X), so the outer conjugate is identity
94
	// - Thus: IFFT(X) = FFT(X) / N (both conjugate operations are implicit/unnecessary)
95
	auto& autocorr = paddedSignal; // Reuse buffer for autocorrelation output
96
	FFTReal<SIGNAL_LEN_L2 + 1>::execute(spectrum, autocorr, fftWork);
97
	// The result is a real signal, so the 2nd half (imaginary part) is zero (apart from rounding errors)
98
99
	// Step 5: Normalize by 1/FFT_LEN to get autocorrelation (optional)
100
	// Since FFT(real+even) = real, no conjugate is needed.
101
	// We only normalize the first N elements that we actually use.
102
	if constexpr (NORMALIZE) {
103
		const float norm = 1.0f / FFT_LEN;
104
		for (unsigned i = 0; i < N; ++i) {
105
			autocorr[i] *= norm;
106
		}
107
	}
108
109
	// Step 6: Extract result
110
	std::ranges::copy_n(autocorr.begin(), N, output.begin());
111
}
112
113
#endif