OXYGENHUB
◆ main openmsx / src / utils / FFTReal.hh
9.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef FFTREAL_HH
2
#define FFTREAL_HH
3
4
// This code is based on:
5
//    fft-real
6
//    https://github.com/cyrilcode/fft-real
7
//    FFTReal is distributed under the terms of the Do What The Fuck You Want To Public License.
8
//
9
// This version is stripped down and specialized for our use case.
10
11
#include "cstd.hh"
12
#include "narrow.hh"
13
#include "xrange.hh"
14
15
#include <array>
16
#include <cassert>
17
#include <cmath>
18
#include <cstdint>
19
#include <numbers>
20
#include <span>
21
22
// Input mode for FFT optimization
23
enum class FFTInputMode {
24
	Full,           // All FFT_LEN input values provided
25
	                // Output: N complex values in FFTReal format (N/2+1 real, N/2-1 imag)
26
	HalfZeroPadded, // Only first FFT_LEN/2 values provided, second half is implicitly zero
27
	                // Example N=8: provide [a,b,c,d] → full array [a,b,c,d,0,0,0,0]
28
	                // Output: N complex values in FFTReal format (N/2+1 real, N/2-1 imag)
29
	Symmetric       // Only first FFT_LEN/2+1 values, mirrored to fill second half
30
	                // input[0] and input[N/2] are unique (endpoints)
31
	                // input[1..N/2-1] mirrored: input[N/2+i] = input[N/2-i]
32
	                // Example N=8: provide [a,b,c,d,e] → full array [a,b,c,d,e,d,c,b]
33
	                // Output: Only N/2+1 REAL values (imaginary parts are ~0 and skipped)
34
};
35
36
// Calculate the FFT for real input (as opposed to complex numbers input).
37
// NOTE: Uses exp(+2πi) sign convention (opposite from numpy/FFTW). Imaginary parts have opposite sign.
38
//
39
// The result however are still complex numbers, and arranged as follows:
40
//   output[0]         = Real(bin 0)
41
//   output[1]         = Real(bin 1)
42
//   ...
43
//   output[len/2 - 1] = Real(bin len/2 -1)
44
//   output[len/2    ] = Real(bin len/2)
45
//   output[len/2 + 1] = Imag(bin 1)
46
//   ...
47
//   output[len   - 2] = Imag(bin len/2 - 2)
48
//   output[len   - 1] = Imag(bin len/2 - 1)
49
// Note: There are two more real-parts than imaginary-parts. For the lowest bin
50
// (the DC-offset) and for the highest bin (at Nyquist frequency) the output is
51
// a pure real value (the imaginary part is zero).
52
template<unsigned FFT_LEN_L2, FFTInputMode MODE = FFTInputMode::Full>
53
class FFTReal
54
{
55
public:
56
	static_assert(FFT_LEN_L2 >=  4); // at least 16-points (allows optimization in final pass)
57
	static_assert(FFT_LEN_L2 <= 16); // at most 65536-points
58
	static constexpr unsigned FFT_LEN = 1 << FFT_LEN_L2;
59
	static constexpr unsigned INPUT_LEN =
60
		(MODE == FFTInputMode::HalfZeroPadded) ? (FFT_LEN / 2) :
61
		(MODE == FFTInputMode::Symmetric) ? (FFT_LEN / 2 + 1) :
62
		FFT_LEN;
63
	static constexpr unsigned OUTPUT_LEN =
64
		(MODE == FFTInputMode::Symmetric) ? (FFT_LEN / 2 + 1) :
65
		FFT_LEN;
66
67
	// Execute FFT and return meaningful output range
68
	// NOTE: The returned span aliases the 'output' parameter (first OUTPUT_LEN elements)
69
	//       Full 'output' buffer is still needed for intermediate calculations
70
	static std::span<float, OUTPUT_LEN> execute(std::span<const float, INPUT_LEN> input,
71
	                                            std::span<float, FFT_LEN> output,
72
	                                            std::span<float, FFT_LEN> tmpBuf)
73
	{
74
		pass<FFT_LEN_L2 - 1>(output, tmpBuf, input);
75
#ifndef NDEBUG
76
		// In debug mode, verify imaginary outputs are indeed ~0 for Symmetric mode
77
		if constexpr (MODE == FFTInputMode::Symmetric) {
78
			for (unsigned i = FFT_LEN / 2 + 1; i < FFT_LEN; ++i) {
79
				assert(std::abs(output[i]) < 1e-4f && "Symmetric mode: imaginary outputs should be ~0");
80
			}
81
		}
82
#endif
83
		return std::span<float, OUTPUT_LEN>(output.data(), OUTPUT_LEN);
84
	}
85
86
private:
87
	// Helper for dependent false in static_assert
88
	template<auto> static inline constexpr bool always_false = false;
89
90
	template<int PASS>
91
	static void pass(std::span<float, FFT_LEN> dst,
92
	                 std::span<float, FFT_LEN> src,
93
	                 std::span<const float, INPUT_LEN> input)
94
	{
95
		if constexpr (PASS == 1) {
96
			// Do 1st and 2nd pass at once.
97
			static constexpr auto len4 = FFT_LEN >> 2;
98
99
			if constexpr (MODE == FFTInputMode::Full) {
100
				// Full input path (general case)
101
				for (unsigned idx = 0; idx < FFT_LEN; idx += 4) {
102
					const unsigned ri_0 = bitRevBuf[idx >> 2];
103
					const unsigned ri_1 = ri_0 + 2 * len4; // bitRevBuf[idx + 1];
104
					const unsigned ri_2 = ri_0 + 1 * len4; // bitRevBuf[idx + 2];
105
					const unsigned ri_3 = ri_0 + 3 * len4; // bitRevBuf[idx + 3];
106
107
					dst[idx + 1] = input[ri_0] - input[ri_1];
108
					dst[idx + 3] = input[ri_2] - input[ri_3];
109
110
					const float sf_0 = input[ri_0] + input[ri_1];
111
					const float sf_2 = input[ri_2] + input[ri_3];
112
113
					dst[idx + 0] = sf_0 + sf_2;
114
					dst[idx + 2] = sf_0 - sf_2;
115
				}
116
			} else if constexpr (MODE == FFTInputMode::HalfZeroPadded) {
117
				// Optimized path: second (implied) half of input is zero
118
				// ri_1 and ri_3 always point to second half, so input[ri_1] == input[ri_3] == 0
119
				// This is a simplified version of the Full case above
120
				for (unsigned idx = 0; idx < FFT_LEN; idx += 4) {
121
					const unsigned ri_0 = bitRevBuf[idx >> 2];
122
					const unsigned ri_2 = ri_0 + 1 * len4; // bitRevBuf[idx + 2];
123
124
					dst[idx + 1] = input[ri_0];
125
					dst[idx + 3] = input[ri_2];
126
					dst[idx + 0] = input[ri_0] + input[ri_2];
127
					dst[idx + 2] = input[ri_0] - input[ri_2];
128
				}
129
			} else if constexpr (MODE == FFTInputMode::Symmetric) {
130
				// Symmetric input: input[0..N/2] provided, input[k] = input[N-k] for k > N/2
131
				// ri_0 and ri_2 always < N/2 (first half, use directly)
132
				// ri_1 and ri_3 always >= N/2 (second half, use mirrored index)
133
				for (unsigned idx = 0; idx < FFT_LEN; idx += 4) {
134
					const unsigned ri_0 = bitRevBuf[idx >> 2];
135
					const unsigned ri_1 = ri_0 + 2 * len4; // bitRevBuf[idx + 1];
136
					const unsigned ri_2 = ri_0 + 1 * len4; // bitRevBuf[idx + 2];
137
					const unsigned ri_3 = ri_0 + 3 * len4; // bitRevBuf[idx + 3];
138
139
					const float in_0 = input[ri_0];
140
					const float in_1 = input[FFT_LEN - ri_1];
141
					const float in_2 = input[ri_2];
142
					const float in_3 = input[FFT_LEN - ri_3];
143
144
					dst[idx + 1] = in_0 - in_1;
145
					dst[idx + 3] = in_2 - in_3;
146
147
					const float sf_0 = in_0 + in_1;
148
					const float sf_2 = in_2 + in_3;
149
150
					dst[idx + 0] = sf_0 + sf_2;
151
					dst[idx + 2] = sf_0 - sf_2;
152
				}
153
			} else {
154
				static_assert(always_false<MODE>, "Unsupported FFTInputMode");
155
			}
156
157
		} else if constexpr (PASS == 2) {
158
			// Start with 1st and 2nd passes (swap source and destination buffers).
159
			pass<1>(src, dst, input);
160
161
			// 3rd pass
162
			static constexpr float sqrt2_2 = std::numbers::sqrt2_v<float> * 0.5f;
163
			for (unsigned idx = 0; idx < FFT_LEN; idx += 8) {
164
				dst[idx + 0] = src[idx] + src[idx + 4];
165
				dst[idx + 4] = src[idx] - src[idx + 4];
166
				dst[idx + 2] = src[idx + 2];
167
				dst[idx + 6] = src[idx + 6];
168
169
				float v1 = (src[idx + 5] - src[idx + 7]) * sqrt2_2;
170
				dst[idx + 1] = src[idx + 1] + v1;
171
				dst[idx + 3] = src[idx + 1] - v1;
172
173
				float v2 = (src[idx + 5] + src[idx + 7]) * sqrt2_2;
174
				dst[idx + 5] = v2 + src[idx + 3];
175
				dst[idx + 7] = v2 - src[idx + 3];
176
			}
177
178
		} else {
179
			// First do the previous passes (swap source and destination buffers).
180
			pass<PASS - 1>(src, dst, input);
181
182
			static constexpr unsigned dist = 1 << (PASS - 1);
183
			static constexpr unsigned c1_r = 0;
184
			static constexpr unsigned c1_i = dist * 1;
185
			static constexpr unsigned c2_r = dist * 2;
186
			static constexpr unsigned c2_i = dist * 3;
187
			static constexpr unsigned cend = dist * 4;
188
			static constexpr unsigned table_step = COS_ARR_SIZE >> (PASS - 1);
189
190
			// For Symmetric input mode on final pass, skip imaginary outputs (they're ~0)
191
			// Final pass: dist = FFT_LEN/4, so c2_i and positions >= c2_r + 1 are imaginary
192
			// Only skip in release mode; in debug mode we compute them to validate they're ~0
193
			[[maybe_unused]] static constexpr bool isFinalPass = (PASS == FFT_LEN_L2 - 1);
194
#ifdef NDEBUG
195
			static constexpr bool skipImaginary = (MODE == FFTInputMode::Symmetric) && isFinalPass;
196
#else
197
			static constexpr bool skipImaginary = false;
198
#endif
199
200
			for (unsigned idx = 0; idx < FFT_LEN; idx += cend) {
201
				std::span<const float> sf = src.subspan(idx);
202
				std::span<      float> df = dst.subspan(idx);
203
204
				// Extreme coefficients are always real
205
				df[c1_r] = sf[c1_r] + sf[c2_r];
206
				df[c2_r] = sf[c1_r] - sf[c2_r];
207
				df[c1_i] = sf[c1_i];
208
				if constexpr (!skipImaginary) {
209
					df[c2_i] = sf[c2_i];  // Imaginary section
210
				}
211
212
				// Others are conjugate complex numbers
213
				for (unsigned i = 1; i < dist; ++ i) {
214
					const float c = cosBuf[        i  * table_step];
215
					const float s = cosBuf[(dist - i) * table_step];
216
217
					const float sf_r_i = sf[c1_r + i];
218
					const float sf_i_i = sf[c1_i + i];
219
220
					const float v1 = sf[c2_r + i] * c - sf[c2_i + i] * s;
221
					df[c1_r + i] = sf_r_i + v1;
222
					df[c2_r - i] = sf_r_i - v1;
223
224
					if constexpr (!skipImaginary) {
225
						const float v2 = sf[c2_r + i] * s + sf[c2_i + i] * c;
226
						df[c2_r + i] = v2 + sf_i_i;
227
						df[cend - i] = v2 - sf_i_i;
228
					}
229
				}
230
			}
231
		}
232
	}
233
234
	static constexpr auto bitRevBuf = []{
235
		constexpr int BR_ARR_SIZE = FFT_LEN / 4;
236
		std::array<uint16_t, BR_ARR_SIZE> result = {};
237
		for (auto cnt : xrange(narrow<unsigned>(result.size()))) {
238
			unsigned index = cnt << 2;
239
			unsigned res = 0;
240
			for (int bit_cnt = FFT_LEN_L2; bit_cnt > 0; --bit_cnt) {
241
				res <<= 1;
242
				res += (index & 1);
243
				index >>= 1;
244
			}
245
			result[cnt] = narrow<uint16_t>(res);
246
		}
247
		return result;
248
	}();
249
250
	static constexpr int COS_ARR_SIZE = FFT_LEN / 4;
251
	static constexpr auto cosBuf = []{
252
		std::array<float, COS_ARR_SIZE> result = {};
253
		const double mul = (0.5 * std::numbers::pi) / COS_ARR_SIZE;
254
		for (auto i : xrange(narrow<unsigned>(result.size()))) {
255
			result[i] = float(cstd::cos<4>(i * mul));
256
		}
257
		return result;
258
	}();
259
};
260
261
#endif