1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
<!DOCTYPE html>
<!--
test_fft.html — Isolated FFT correctness tests for fft.js
Open directly in a browser (no server needed).
Tests fftForward(), realFFT(), and STFTCache from fft.js.
Test summary:
1 DC impulse — all-ones input → bin 0 must equal N
2 Single tone — 440 Hz pure sine → peak bin matches expected frequency
3 STFT magnitude — STFTCache.getMagnitudeDB() returns loud value at 440 Hz
4 Pair equal — 220 + 880 Hz, both peaks found
5 Pair equal — 440 + 1320 Hz, both peaks found
6 Pair wide — 300 + 3000 Hz (decade separation), both peaks found
7 Pair extreme — 100 + 8000 Hz (80× ratio), both peaks found
8 Pair unequal — 440 Hz + 2000 Hz at 10:1 amplitude, weak peak still found
9 Triplet chord — C4 (261.63) + E4 (329.63) + G4 (392) major chord
10 Triplet octaves — 110 + 220 + 440 Hz octave stack
11 Triplet harmonic — 500 + 1500 + 4500 Hz (1:3:9 ratio)
12 Triplet unequal — 440 + 880 + 1760 Hz at 1.0 / 0.5 / 0.25 (decaying harmonics)
Pass criteria:
- Each expected frequency bin is found within ±guard bins (guard ≈ 2 × bin_width).
- Bin width = SR / N = 32000 / 4096 ≈ 7.8 Hz.
All spectra are drawn as linear-magnitude plots (0..Nyquist on x-axis).
Colored vertical markers show expected frequency positions.
-->
<html>
<head>
<meta charset="utf-8">
<title>FFT Test</title>
<style>
body { font-family: monospace; background: #111; color: #ccc; padding: 20px; }
h2 { color: #fff; }
canvas { border: 1px solid #444; display: block; margin: 10px 0; }
.pass { color: #4f4; }
.fail { color: #f44; }
pre { background: #222; padding: 10px; }
</style>
</head>
<body>
<h2>FFT Isolation Test</h2>
<pre id="log"></pre>
<canvas id="spectrum" width="800" height="200"></canvas>
<script src="fft.js"></script>
<script>
const log = document.getElementById('log');
function print(msg, cls) {
const span = document.createElement('span');
if (cls) span.className = cls;
span.textContent = msg + '\n';
log.appendChild(span);
}
// --- Test 1: Single bin impulse (DC) ---
{
const N = 8;
const real = new Float32Array([1,1,1,1,1,1,1,1]);
const imag = new Float32Array(N);
fftForward(real, imag, N);
const ok = Math.abs(real[0] - 8) < 1e-4 && Math.abs(imag[0]) < 1e-4;
print(`Test 1 DC impulse: real[0]=${real[0].toFixed(4)} ${ok?'PASS':'FAIL'}`, ok?'pass':'fail');
}
// --- Test 2: 440 Hz peak detection ---
{
const SR = 32000;
const N = 2048;
const signal = new Float32Array(N);
for (let i = 0; i < N; ++i)
signal[i] = Math.sin(2 * Math.PI * 440 * i / SR);
const spectrum = realFFT(signal);
let peakBin = 0, peakVal = 0;
for (let i = 0; i < N / 2; ++i) {
const re = spectrum[i * 2], im = spectrum[i * 2 + 1];
const mag = re * re + im * im;
if (mag > peakVal) { peakVal = mag; peakBin = i; }
}
const peakFreq = peakBin * SR / N;
const ok = Math.abs(peakFreq - 440) < SR / N;
print(`Test 2 440Hz peak: bin=${peakBin} freq=${peakFreq.toFixed(1)}Hz ${ok?'PASS':'FAIL'}`, ok?'pass':'fail');
// Draw spectrum
const canvas = document.getElementById('spectrum');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#0af';
ctx.beginPath();
const halfN = N / 2;
for (let i = 0; i < halfN; ++i) {
const re = spectrum[i * 2], im = spectrum[i * 2 + 1];
const mag = Math.sqrt(re * re + im * im) / (N / 2);
const x = i / halfN * canvas.width;
const y = canvas.height - mag * canvas.height;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Mark 440Hz
ctx.strokeStyle = '#f80';
ctx.beginPath();
const x440 = peakBin / halfN * canvas.width;
ctx.moveTo(x440, 0); ctx.lineTo(x440, canvas.height);
ctx.stroke();
ctx.fillStyle = '#f80';
ctx.fillText(`440Hz (bin ${peakBin})`, x440 + 4, 16);
}
// --- Test 3: STFT getMagnitudeDB at t=0 ---
{
const SR = 32000;
const N = 44032; // ~1.375s
const signal = new Float32Array(N);
for (let i = 0; i < N; ++i)
signal[i] = Math.sin(2 * Math.PI * 440 * i / SR);
const stft = new STFTCache(signal, SR, 2048, 512);
const db = stft.getMagnitudeDB(0.0, 440);
const ok = db > -10;
print(`Test 3 STFT getMagnitudeDB(0, 440Hz): ${db.toFixed(1)} dB ${ok?'PASS':'FAIL'}`, ok?'pass':'fail');
}
// Helper: find top-K peaks in spectrum (bin indices), ignoring neighbors within 'guard' bins
function findPeaks(spectrum, halfN, k, guard) {
const mags = new Float32Array(halfN);
for (let i = 0; i < halfN; ++i) {
const re = spectrum[i * 2], im = spectrum[i * 2 + 1];
mags[i] = re * re + im * im;
}
const peaks = [];
const used = new Uint8Array(halfN);
for (let p = 0; p < k; ++p) {
let best = -1, bestVal = 0;
for (let i = 1; i < halfN; ++i) {
if (!used[i] && mags[i] > bestVal) { bestVal = mags[i]; best = i; }
}
if (best < 0) break;
peaks.push(best);
for (let g = Math.max(0, best - guard); g <= Math.min(halfN - 1, best + guard); ++g)
used[g] = 1;
}
return peaks;
}
// Helper: draw labeled spectrum on a new canvas
function drawSpectrum(label, spectrum, N, SR, markerFreqs) {
const halfN = N / 2;
const colors = ['#f80', '#0f8', '#f0f', '#08f'];
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 160;
const div = document.createElement('div');
div.style.cssText = 'color:#888;font-family:monospace;font-size:12px;margin-top:8px';
div.textContent = label;
document.body.appendChild(div);
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#0af';
ctx.beginPath();
for (let i = 0; i < halfN; ++i) {
const re = spectrum[i * 2], im = spectrum[i * 2 + 1];
const mag = Math.sqrt(re * re + im * im) / (N / 2);
const x = i / halfN * canvas.width;
const y = canvas.height - mag * canvas.height;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
markerFreqs.forEach((f, idx) => {
const bin = Math.round(f * N / SR);
const x = bin / halfN * canvas.width;
ctx.strokeStyle = colors[idx % colors.length];
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
ctx.fillStyle = colors[idx % colors.length];
ctx.fillText(`${f}Hz`, x + 3, 14 + idx * 14);
});
}
// Helper: test multi-frequency signal, return pass/fail
function testMultiFreq(label, freqs, amplitudes, SR, N, testNum) {
const signal = new Float32Array(N);
for (let i = 0; i < N; ++i)
for (let f = 0; f < freqs.length; ++f)
signal[i] += (amplitudes ? amplitudes[f] : 1.0) * Math.sin(2 * Math.PI * freqs[f] * i / SR);
const spectrum = realFFT(signal);
const halfN = N / 2;
const guard = Math.ceil(SR / N * 2); // ~2 bins tolerance
const peaks = findPeaks(spectrum, halfN, freqs.length, guard);
const detectedFreqs = peaks.map(b => (b * SR / N).toFixed(1));
const allFound = freqs.every(f => {
const expectedBin = Math.round(f * N / SR);
return peaks.some(b => Math.abs(b - expectedBin) <= guard);
});
const freqStr = freqs.map((f, i) => amplitudes ? `${f}Hz@${amplitudes[i].toFixed(1)}` : `${f}Hz`).join(' + ');
print(`Test ${testNum} ${label} [${freqStr}]: peaks=[${detectedFreqs.join(', ')}] ${allFound?'PASS':'FAIL'}`,
allFound ? 'pass' : 'fail');
drawSpectrum(`Test ${testNum}: ${label} — ${freqStr}`, spectrum, N, SR, freqs);
return allFound;
}
const SR = 32000, N = 4096;
// --- Pairs ---
print('\n-- Pairs --');
testMultiFreq('pair', [220, 880], null, SR, N, 4);
testMultiFreq('pair', [440, 1320], null, SR, N, 5);
testMultiFreq('pair', [300, 3000], null, SR, N, 6);
testMultiFreq('pair', [100, 8000], null, SR, N, 7);
testMultiFreq('pair unequal amp', [440, 2000], [1.0, 0.1], SR, N, 8);
// --- Triplets ---
print('\n-- Triplets --');
testMultiFreq('triplet', [261.63, 329.63, 392.00], null, SR, N, 9); // C E G chord
testMultiFreq('triplet', [110, 220, 440], null, SR, N, 10); // octave stack
testMultiFreq('triplet', [500, 1500, 4500], null, SR, N, 11); // harmonic series
testMultiFreq('triplet unequal', [440, 880, 1760], [1.0, 0.5, 0.25],SR, N, 12); // decaying harmonics
</script>
</body>
</html>
|