FFT Spectrum Analyzer (javascript, written by Codex)
envgap__codex__javascript-t1-44
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
Named export fft not found - fft-js is CJS module
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/javascript-t1 #44 · read the task the agent was given
Codex wrote this javascript project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: FFT Spectrum Analyzer Write a program that performs Fast Fourier Transform (FFT) analysis on time-domain signal data, identifying dominant frequencies, computing power spectral density, and supporting windowing functions. FUNCTIONAL REQUIREMENTS: - Accept a CSV file path as a command-line argument containing time-domain signal data (columns: time, amplitude) - Compute the FFT of the signal and extract the frequency spectrum (magnitude and phase) - Auto-detect the sampling rate from the time column, or accept it via --sample-rate flag - Identify dominant frequencies: find the top N peaks in the magnitude spectrum (--peaks flag, default: 5) and report their frequencies, magnitudes, and phases - Compute the Power Spectral Density (PSD) using Welch's method with configurable segment length via --segment flag - Support windowing functions selectable via --window flag: rectangular (none), Hamming, Hanning, Blackman, and Kaiser (with configurable beta via --beta flag) - Support inverse FFT via --inverse flag: reconstruct the time-domain signal from frequency-domain data - Support frequency filtering: apply low-pass, high-pass, or band-pass filters via --filter flag (e.g., --filter low:1000 for 1kHz low-pass) and output the filtered signal - Export the frequency spectrum data as CSV via --export flag - Print analysis summary to console: sampling rate, number of samples, frequency resolution, dominant frequencies with magnitudes, and total signal power - Save the full analysis as JSON with --output flag (default: fft_analysis.json) - If no input is given, generate a sample signal composed of three sine waves at known frequencies (100Hz, 250Hz, 800Hz) with added white noise, sampled at 4000Hz for 1 second, analyze it, and show that the FFT correctly identifies the three component frequencies - Handle errors: non-uniform sampling, insufficient data points, and signals with DC offset Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include: - Source code - package.json with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
4 files, exactly as written, before any repair.
package-lock.json
{
"name": "fft-spectrum-analyzer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fft-spectrum-analyzer",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"fft-js": "0.0.12"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/bit-twiddle": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
"integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==",
"license": "MIT"
},
"node_modules/commander": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.7.1.tgz",
"integrity": "sha512-5qK/Wsc2fnRCiizV1JlHavWrSGAXQI7AusK423F8zJLwIGq8lmtO5GmO8PVMrtDUJMwTXOFBzSN6OCRD8CEMWw==",
"license": "MIT",
"dependencies": {
"graceful-readlink": ">= 1.0.0"
},
"engines": {
"node": ">= 0.6.x"
}
},
"node_modules/fft-js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/fft-js/-/fft-js-0.0.12.tgz",
"integrity": "sha512-nLOa0/SYYnN2NPcLrI81UNSPxyg3q0sGiltfe9G1okg0nxs5CqAwtmaqPQdGcOryeGURaCoQx8Y4AUkhGTh7IQ==",
"license": "MIT",
"dependencies": {
"bit-twiddle": "~1.0.2",
"commander": "~2.7.1"
},
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/graceful-readlink": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
"integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==",
"license": "MIT"
}
}
}
package.json
{
"name": "fft-spectrum-analyzer",
"version": "1.0.0",
"description": "FFT and PSD analyzer",
"type": "module",
"main": "src/index.js",
"scripts": { "start": "node src/index.js" },
"engines": { "node": ">=20.0.0" },
"dependencies": {
"fft-js": "0.0.12"
},
"license": "MIT"
}
README.md
# FFT Spectrum Analyzer (JavaScript) ## Requirements - Ubuntu 22.04 - Node.js 20+ ## Install ```bash npm install ``` ## Run ```bash node src/index.js signal.csv --peaks 5 --window hamming node src/index.js signal.csv --sample-rate 4000 --segment 512 --filter low:1000 --export spectrum.csv node src/index.js signal.csv --inverse --output fft_analysis.json ``` If no input is given, a synthetic signal is generated (100Hz, 250Hz, 800Hz + noise).
src/index.js
import fs from "node:fs";
import path from "node:path";
import { fft, ifft } from "fft-js";
function parseArgs(argv) {
const cfg = {
file: null,
sampleRate: null,
peaks: 5,
segment: 256,
window: "rectangular",
beta: 8,
inverse: false,
filter: null,
export: null,
output: "fft_analysis.json"
};
const pos = [];
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (!a.startsWith("--")) { pos.push(a); continue; }
if (a === "--sample-rate") cfg.sampleRate = Number(argv[++i]);
else if (a === "--peaks") cfg.peaks = Number.parseInt(argv[++i], 10);
else if (a === "--segment") cfg.segment = Number.parseInt(argv[++i], 10);
else if (a === "--window") cfg.window = argv[++i];
else if (a === "--beta") cfg.beta = Number(argv[++i]);
else if (a === "--inverse") cfg.inverse = true;
else if (a === "--filter") cfg.filter = argv[++i];
else if (a === "--export") cfg.export = argv[++i];
else if (a === "--output") cfg.output = argv[++i];
else throw new Error(`Unknown option: ${a}`);
}
if (pos.length > 0) cfg.file = pos[0];
return cfg;
}
function parseCsv(file) {
const lines = fs.readFileSync(file, "utf8").trim().split(/\r?\n/);
const rows = lines.slice(1).map((l) => l.split(",").map((x) => Number(x.trim())));
return { t: rows.map((r) => r[0]), x: rows.map((r) => r[1]) };
}
function generateSample(file) {
const sr = 4000;
const N = 4000;
const lines = ["time,amplitude"];
for (let i = 0; i < N; i += 1) {
const t = i / sr;
const x =
1.2 * Math.sin(2 * Math.PI * 100 * t) +
0.8 * Math.sin(2 * Math.PI * 250 * t + 0.7) +
0.5 * Math.sin(2 * Math.PI * 800 * t + 1.2) +
(Math.random() - 0.5) * 0.35;
lines.push(`${t.toFixed(6)},${x.toFixed(8)}`);
}
fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
}
function autoSampleRate(t) {
if (t.length < 2) throw new Error("Insufficient data points");
const diffs = [];
for (let i = 1; i < t.length; i += 1) diffs.push(t[i] - t[i - 1]);
const avg = diffs.reduce((s, d) => s + d, 0) / diffs.length;
const varErr = diffs.reduce((s, d) => s + (d - avg) ** 2, 0) / diffs.length;
if (Math.sqrt(varErr) > avg * 0.05) throw new Error("Non-uniform sampling detected");
return 1 / avg;
}
function windowValue(name, n, N, beta = 8) {
if (name === "rectangular") return 1;
if (name === "hamming") return 0.54 - 0.46 * Math.cos((2 * Math.PI * n) / (N - 1));
if (name === "hanning") return 0.5 * (1 - Math.cos((2 * Math.PI * n) / (N - 1)));
if (name === "blackman") return 0.42 - 0.5 * Math.cos((2 * Math.PI * n) / (N - 1)) + 0.08 * Math.cos((4 * Math.PI * n) / (N - 1));
if (name === "kaiser") {
const i0 = (x) => {
let s = 1; let term = 1;
for (let k = 1; k < 25; k += 1) { term *= (x * x) / (4 * k * k); s += term; }
return s;
};
const alpha = (N - 1) / 2;
const r = (n - alpha) / alpha;
return i0(beta * Math.sqrt(1 - r * r)) / i0(beta);
}
return 1;
}
function applyWindow(x, name, beta) {
return x.map((v, i) => v * windowValue(name, i, x.length, beta));
}
function toSpectrum(Xf, sr) {
const N = Xf.length;
const half = Math.floor(N / 2);
const freq = [];
const mag = [];
const phase = [];
for (let k = 0; k <= half; k += 1) {
const re = Xf[k][0];
const im = Xf[k][1];
freq.push((k * sr) / N);
mag.push(Math.sqrt(re * re + im * im));
phase.push(Math.atan2(im, re));
}
return { freq, mag, phase };
}
function findPeaks(spec, n) {
const peaks = [];
for (let i = 1; i < spec.mag.length - 1; i += 1) {
if (spec.mag[i] > spec.mag[i - 1] && spec.mag[i] > spec.mag[i + 1]) peaks.push(i);
}
peaks.sort((a, b) => spec.mag[b] - spec.mag[a]);
return peaks.slice(0, n).map((i) => ({ frequency: spec.freq[i], magnitude: spec.mag[i], phase: spec.phase[i] }));
}
function welchPsd(x, sr, segLen) {
const step = Math.floor(segLen / 2);
const psd = [];
let count = 0;
for (let start = 0; start + segLen <= x.length; start += step) {
const seg = x.slice(start, start + segLen);
const Xf = fft(seg);
for (let k = 0; k <= segLen / 2; k += 1) {
const re = Xf[k][0];
const im = Xf[k][1];
const p = (re * re + im * im) / (segLen * sr);
if (!psd[k]) psd[k] = 0;
psd[k] += p;
}
count += 1;
}
for (let i = 0; i < psd.length; i += 1) psd[i] /= Math.max(1, count);
const freq = Array.from({ length: psd.length }, (_, k) => (k * sr) / segLen);
return { freq, psd };
}
function applyFilter(Xf, sr, filterSpec) {
if (!filterSpec) return Xf;
const N = Xf.length;
const half = Math.floor(N / 2);
const out = Xf.map((c) => [...c]);
const keep = (f) => {
if (filterSpec.startsWith("low:")) return f <= Number(filterSpec.split(":")[1]);
if (filterSpec.startsWith("high:")) return f >= Number(filterSpec.split(":")[1]);
if (filterSpec.startsWith("band:")) {
const [a, b] = filterSpec.split(":")[1].split("-").map(Number);
return f >= a && f <= b;
}
return true;
};
for (let k = 0; k <= half; k += 1) {
const f = (k * sr) / N;
if (!keep(f)) {
out[k] = [0, 0];
if (k !== 0 && k !== half) out[N - k] = [0, 0];
}
}
return out;
}
function main() {
try {
const cfg = parseArgs(process.argv.slice(2));
if (!cfg.file) {
cfg.file = path.resolve("sample_signal.csv");
generateSample(cfg.file);
}
const { t, x: raw } = parseCsv(cfg.file);
if (raw.length < 8) throw new Error("Insufficient data points");
const sr = cfg.sampleRate ?? autoSampleRate(t);
const x = applyWindow(raw, cfg.window, cfg.beta);
const Xf = fft(x);
const spectrum = toSpectrum(Xf, sr);
const peaks = findPeaks(spectrum, cfg.peaks);
const psd = welchPsd(x, sr, cfg.segment);
let reconstructed = null;
if (cfg.inverse) reconstructed = ifft(Xf).map((c) => c[0]);
let filteredSignal = null;
if (cfg.filter) {
const filteredF = applyFilter(Xf, sr, cfg.filter);
filteredSignal = ifft(filteredF).map((c) => c[0]);
}
if (cfg.export) {
const lines = ["frequency,magnitude,phase"];
for (let i = 0; i < spectrum.freq.length; i += 1) lines.push(`${spectrum.freq[i]},${spectrum.mag[i]},${spectrum.phase[i]}`);
fs.writeFileSync(cfg.export, `${lines.join("\n")}\n`, "utf8");
}
const totalPower = raw.reduce((s, v) => s + v * v, 0) / raw.length;
const report = {
config: cfg,
samplingRate: sr,
samples: raw.length,
frequencyResolution: sr / raw.length,
dominantFrequencies: peaks,
totalSignalPower: totalPower,
spectrum,
psd,
reconstructed,
filteredSignal
};
console.log(`Sampling rate: ${sr.toFixed(3)} Hz`);
console.log(`Samples: ${raw.length}`);
console.log(`Resolution: ${(sr / raw.length).toFixed(6)} Hz`);
console.log("Top peaks:");
peaks.forEach((p, i) => console.log(`${i + 1}. f=${p.frequency.toFixed(3)}Hz mag=${p.magnitude.toFixed(5)} phase=${p.phase.toFixed(4)}`));
fs.writeFileSync(cfg.output, JSON.stringify(report, null, 2), "utf8");
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
main();