Skip to content

Commit 28c6646

Browse files
committed
Improved FFT
1 parent 3da87e1 commit 28c6646

File tree

2 files changed

+210
-71
lines changed

2 files changed

+210
-71
lines changed

src/components/BandPowerGraph.tsx

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,41 +47,69 @@ const Graph
4747
],
4848
[]
4949
);
50+
const DELTA_RANGE = [0, 4],
51+
THETA_RANGE = [4, 8],
52+
ALPHA_RANGE = [8, 12],
53+
BETA_RANGE = [12, 30],
54+
GAMMA_RANGE = [30, 100];
55+
56+
57+
const FREQ_RESOLUTION = samplingRate / 256;
58+
function calculateBandPower(fftMagnitudes:number[], freqRange:number[]) {
59+
const [startFreq, endFreq] = freqRange;
60+
const startIndex = Math.max(1, Math.floor(startFreq / FREQ_RESOLUTION));
61+
const endIndex = Math.min(Math.floor(endFreq / FREQ_RESOLUTION), fftMagnitudes.length - 1);
62+
let power = 0;
63+
for (let i = startIndex; i <= endIndex; i++) {
64+
power += fftMagnitudes[i] * fftMagnitudes[i];
65+
}
66+
return power;
67+
}
68+
let buffer_size = 32;
69+
let circular_buffer = new Array(buffer_size).fill(0);
70+
let data_index = 0, sum = 0;
71+
72+
class SmoothedBeta {
73+
private bufferSize: number;
74+
private circularBuffer: number[];
75+
private sum: number;
76+
private dataIndex: number;
77+
78+
constructor(bufferSize: number) {
79+
this.bufferSize = bufferSize;
80+
this.circularBuffer = new Array(bufferSize).fill(0);
81+
this.sum = 0;
82+
this.dataIndex = 0;
83+
}
84+
85+
getSmoothedBeta(beta: number): number {
86+
this.sum -= this.circularBuffer[this.dataIndex];
87+
this.sum += beta;
88+
this.circularBuffer[this.dataIndex] = beta;
89+
this.dataIndex = (this.dataIndex + 1) % this.bufferSize;
90+
return this.sum / this.bufferSize;
91+
}
92+
}
93+
94+
// Example usage:
95+
const smoother1 = new SmoothedBeta(buffer_size);
96+
const smoother2 = new SmoothedBeta(buffer_size);
97+
const smoother3= new SmoothedBeta(buffer_size);
98+
const smoother4 = new SmoothedBeta(buffer_size);
99+
const smoother5= new SmoothedBeta(buffer_size);
50100

51-
const calculateBandPower = useCallback(
52-
(fftChannelData: number[]) => {
53-
const freqResolution = samplingRate / (fftChannelData.length * 2);
54-
55-
return bandRanges.map(([low, high]) => {
56-
const startIndex = Math.max(1, Math.floor(low / freqResolution));
57-
const endIndex = Math.min(
58-
Math.ceil(high / freqResolution),
59-
fftChannelData.length - 1
60-
);
61-
62-
let bandPower = 0;
63-
for (let i = startIndex; i <= endIndex; i++) {
64-
if (!isNaN(fftChannelData[i]) && i < fftChannelData.length) {
65-
bandPower += Math.pow(fftChannelData[i], 2); // Use square of magnitude
66-
}
67-
}
68-
69-
// Normalize by the number of frequency bins in the band
70-
const normalizedPower = bandPower / (endIndex - startIndex + 1);
71-
72-
// Convert to dB
73-
const powerDB = 10 * Math.log10(normalizedPower);
74-
75-
return powerDB;
76-
});
77-
},
78-
[bandRanges, samplingRate]
79-
);
80101

81102
useEffect(() => {
82103
if (fftData.length > 0 && fftData[0].length > 0) {
83104
const channelData = fftData[0];
84-
const newBandPowerData = calculateBandPower(channelData);
105+
106+
const deltaPower = calculateBandPower(channelData, DELTA_RANGE);
107+
const thetaPower = calculateBandPower(channelData, THETA_RANGE);
108+
const alphaPower = calculateBandPower(channelData, ALPHA_RANGE);
109+
const betaPower = calculateBandPower(channelData, BETA_RANGE);
110+
const gammaPower = calculateBandPower(channelData, GAMMA_RANGE)
111+
const total=deltaPower+thetaPower+alphaPower+betaPower+gammaPower;
112+
const newBandPowerData = [(deltaPower / total)*100,(thetaPower / total)*100,(alphaPower / total)*100,(betaPower / total)*100,(gammaPower / total)*100];
85113

86114
if (
87115
newBandPowerData.some((value) => !isNaN(value) && value > -Infinity)
@@ -117,7 +145,7 @@ const Graph
117145
ctx.clearRect(0, 0, width, height);
118146

119147
const barWidth = (width - 70) / bandNames.length;
120-
let minPower = Math.min(...currentBandPowerData);
148+
let minPower = Math.min(0);
121149
let maxPower = Math.max(...currentBandPowerData);
122150

123151
if (maxPower - minPower < 1) {
@@ -179,7 +207,6 @@ const Graph
179207

180208
const animateGraph = useCallback(() => {
181209
const interpolationFactor = 0.1;
182-
183210
const currentValues = bandPowerData.map((target, i) => {
184211
const prev = prevBandPowerData.current[i];
185212
return prev + (target - prev) * interpolationFactor;

src/components/FFT.tsx

Lines changed: 151 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import React, {
88
useCallback,
99
} from "react";
1010
import { useTheme } from "next-themes";
11-
import { BitSelection } from "./DataPass";
1211
import BandPowerGraph from "./BandPowerGraph";
1312
import { fft } from "fft-js";
1413
import { WebglPlot, ColorRGBA, WebglLine } from "webgl-plot";
@@ -34,8 +33,7 @@ const FFT = forwardRef(
3433
) => {
3534
const fftBufferRef = useRef<number[][]>(Array.from({ length: 16 }, () => []));
3635
const [fftData, setFftData] = useState<number[][]>(Array.from({ length: 16 }, () => []));
37-
const fftSize = currentSamplingRate + 6 * (currentSamplingRate / 250);
38-
// console.log(fftSize);
36+
const fftSize = Math.pow(2, Math.round(Math.log2(currentSamplingRate / 2)));
3937
const canvasRef = useRef<HTMLCanvasElement>(null);
4038
const containerRef = useRef<HTMLDivElement>(null);
4139
const { theme } = useTheme();
@@ -47,43 +45,157 @@ const FFT = forwardRef(
4745
const wglPlotsref = useRef<WebglPlot[]>([]);
4846
const linesRef = useRef<WebglLine[]>([]);
4947
const sweepPositions = useRef<number[]>(new Array(6).fill(0)); // Array for sweep positions
50-
48+
let samplesReceived = 0;
49+
class SmoothingFilter {
50+
private bufferSize: number;
51+
private circularBuffers: number[][];
52+
private sums: number[];
53+
private dataIndex: number = 0;
54+
55+
constructor(bufferSize: number = 5, initialLength: number = 0) {
56+
this.bufferSize = bufferSize;
57+
this.circularBuffers = Array.from({ length: initialLength }, () =>
58+
new Array(bufferSize).fill(0)
59+
);
60+
this.sums = new Array(initialLength).fill(0);
61+
}
62+
63+
getSmoothedValues(newValues: number[]): number[] {
64+
// Initialize buffers if first run or size changed
65+
if (this.circularBuffers.length !== newValues.length) {
66+
this.circularBuffers = Array.from({ length: newValues.length }, () =>
67+
new Array(this.bufferSize).fill(0)
68+
);
69+
this.sums = new Array(newValues.length).fill(0);
70+
}
71+
72+
const smoothed = new Array(newValues.length);
73+
74+
for (let i = 0; i < newValues.length; i++) {
75+
this.sums[i] -= this.circularBuffers[i][this.dataIndex];
76+
this.sums[i] += newValues[i];
77+
this.circularBuffers[i][this.dataIndex] = newValues[i];
78+
smoothed[i] = this.sums[i] / this.bufferSize;
79+
}
80+
81+
this.dataIndex = (this.dataIndex + 1) % this.bufferSize;
82+
return smoothed;
83+
}
84+
}
85+
const filter = new SmoothingFilter(32,fftSize/2); // 5-point moving average
5186

5287
useImperativeHandle(
5388
ref,
5489
() => ({
5590
updateData(data: number[]) {
5691
for (let i = 0; i < 1; i++) {
57-
5892
const sensorValue = data[i + 1];
93+
// Add new sample to the buffer
5994
fftBufferRef.current[i].push(sensorValue);
95+
// Update the plot with the new sensor value
6096
updatePlot(sensorValue, Zoom);
97+
// Ensure the buffer does not exceed fftSize
98+
if (fftBufferRef.current[i].length > fftSize) {
99+
fftBufferRef.current[i].shift(); // Remove the oldest sample
100+
}
101+
samplesReceived++;
61102

62-
if (fftBufferRef.current[i].length >= fftSize) {
103+
// Trigger FFT computation every 5 samples
104+
if (samplesReceived % 25 === 0 ) {
63105
const processedBuffer = fftBufferRef.current[i].slice(0, fftSize); // Ensure exact length
64-
const dcRemovedBuffer = removeDCComponent(processedBuffer);
65-
const filteredBuffer = applyHighPassFilter(dcRemovedBuffer, 0.5); // 0.5 Hz cutoff
66-
const windowedBuffer = applyHannWindow(filteredBuffer);
67-
const complexFFT = fft(windowedBuffer); // Perform FFT
68-
const magnitude = complexFFT.map(([real, imaginary]) =>
69-
Math.sqrt(real ** 2 + imaginary ** 2)
70-
); // Calculate the magnitude
71-
// console.log("magnitude", complexFFT);
72-
const freqs = Array.from({ length: fftSize / 2 }, (_, i) => (i * currentSamplingRate) / fftSize);
73-
// console.log(freqs);
106+
const floatInput = new Float32Array(processedBuffer);
107+
108+
// const dcRemovedBuffer = removeDCComponent(processedBuffer);
109+
// const filteredBuffer = applyHighPassFilter(dcRemovedBuffer, 0.5); // 0.5 Hz cutoff
110+
// const windowedBuffer = applyHannWindow(filteredBuffer);
111+
// const complexFFT = fft(windowedBuffer); // Perform FFT
112+
// const magnitude = complexFFT.map(([real, imaginary]) =>
113+
// Math.sqrt(real ** 2 + imaginary ** 2)
114+
// );
115+
// Calculate frequencies for the FFT result
116+
const fftMags = fftProcessor.computeMagnitudes(floatInput);
117+
const magArray = Array.from(fftMags); // Convert Float32Array to regular array
118+
const smoothedMags = filter.getSmoothedValues(magArray);
119+
// Update the FFT data state
74120
setFftData((prevData) => {
75121
const newData = [...prevData];
76-
newData[i] = magnitude.slice(0, fftSize / 2); // Assign to the corresponding channel
122+
newData[i] = smoothedMags;
77123
return newData;
78124
});
79125

80-
fftBufferRef.current[i] = []; // Clear buffer after processing
126+
// Clear the buffer after processing (optional, depending on your use case)
127+
// fftBufferRef.current[i] = [];
81128
}
82129
}
83130
},
84131
}),
85-
[Zoom, timeBase, canvasCount]
132+
[Zoom, timeBase, canvasCount, fftSize, currentSamplingRate]
86133
);
134+
135+
////
136+
class FFT {
137+
private size: number;
138+
private cosTable: Float32Array;
139+
private sinTable: Float32Array;
140+
141+
constructor(size: number) {
142+
this.size = size;
143+
this.cosTable = new Float32Array(size / 2);
144+
this.sinTable = new Float32Array(size / 2);
145+
for (let i = 0; i < size / 2; i++) {
146+
this.cosTable[i] = Math.cos(-2 * Math.PI * i / size);
147+
this.sinTable[i] = Math.sin(-2 * Math.PI * i / size);
148+
}
149+
}
150+
151+
computeMagnitudes(input: Float32Array): Float32Array {
152+
const real = new Float32Array(this.size);
153+
const imag = new Float32Array(this.size);
154+
for (let i = 0; i < input.length && i < this.size; i++) {
155+
real[i] = input[i];
156+
}
157+
this.fft(real, imag);
158+
const mags = new Float32Array(this.size / 2);
159+
for (let i = 0; i < this.size / 2; i++) {
160+
mags[i] = Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) / (this.size / 2);
161+
}
162+
return mags;
163+
}
164+
165+
private fft(real: Float32Array, imag: Float32Array): void {
166+
const n = this.size;
167+
let j = 0;
168+
for (let i = 0; i < n - 1; i++) {
169+
if (i < j) {
170+
[real[i], real[j]] = [real[j], real[i]];
171+
[imag[i], imag[j]] = [imag[j], imag[i]];
172+
}
173+
let k = n / 2;
174+
while (k <= j) { j -= k; k /= 2; }
175+
j += k;
176+
}
177+
for (let l = 2; l <= n; l *= 2) {
178+
const le2 = l / 2;
179+
for (let k = 0; k < le2; k++) {
180+
const kth = k * (n / l);
181+
const c = this.cosTable[kth], s = this.sinTable[kth];
182+
for (let i = k; i < n; i += l) {
183+
const i2 = i + le2;
184+
const tr = c * real[i2] - s * imag[i2];
185+
const ti = c * imag[i2] + s * real[i2];
186+
real[i2] = real[i] - tr;
187+
imag[i2] = imag[i] - ti;
188+
real[i] += tr;
189+
imag[i] += ti;
190+
}
191+
}
192+
}
193+
}
194+
}
195+
196+
197+
const fftProcessor = new FFT(fftSize);
198+
87199
///
88200
const createCanvasElement = () => {
89201

@@ -253,7 +365,7 @@ const FFT = forwardRef(
253365

254366
const xScale = (width - leftMargin - 10) / displayPoints;
255367

256-
let yMax = 1; // Default to prevent division by zero
368+
let yMax = 0; // Default to prevent division by zero
257369
fftData.forEach((channelData) => {
258370
if (channelData.length > 0) {
259371
yMax = Math.max(yMax, ...channelData.slice(0, displayPoints));
@@ -326,27 +438,27 @@ const FFT = forwardRef(
326438

327439
return (
328440
<div className="flex flex-col w-full h-screen overflow-hidden">
329-
{/* Plotting Data / Main content area */}
330-
<main
331-
ref={canvasContainerRef}
332-
className="flex-1 bg-highlight rounded-2xl m-2 overflow-hidden min-h-0"
333-
>
334-
{/* Main content goes here */}
335-
</main>
336-
337-
{/* Responsive container for FFT (canvas) and BandPowerGraph */}
338-
<div className="flex-1 m-2 flex flex-col md:flex-row justify-center overflow-hidden min-h-0 gap-12">
339-
340-
{/* FFT Canvas container */}
341-
<div ref={containerRef} className="flex-1 overflow-hidden min-h-0 min-w-0 ml-12 ">
342-
<canvas ref={canvasRef} className="w-full h-full" />
343-
</div>
344-
{/* BandPowerGraph container */}
345-
<div className="flex-1 overflow-hidden min-h-0 min-w-0 ml-4">
346-
<BandPowerGraph fftData={fftData} samplingRate={currentSamplingRate} />
441+
{/* Plotting Data / Main content area */}
442+
<main
443+
ref={canvasContainerRef}
444+
className="flex-1 bg-highlight rounded-2xl m-2 overflow-hidden min-h-0"
445+
>
446+
{/* Main content goes here */}
447+
</main>
448+
449+
{/* Responsive container for FFT (canvas) and BandPowerGraph */}
450+
<div className="flex-1 m-2 flex flex-col md:flex-row justify-center overflow-hidden min-h-0 gap-12">
451+
452+
{/* FFT Canvas container */}
453+
<div ref={containerRef} className="flex-1 overflow-hidden min-h-0 min-w-0 ml-12 ">
454+
<canvas ref={canvasRef} className="w-full h-full" />
455+
</div>
456+
{/* BandPowerGraph container */}
457+
<div className="flex-1 overflow-hidden min-h-0 min-w-0 ml-4">
458+
<BandPowerGraph fftData={fftData} samplingRate={currentSamplingRate} />
459+
</div>
347460
</div>
348461
</div>
349-
</div>
350462
);
351463
}
352464
);

0 commit comments

Comments
 (0)