Back to Blog

MATLAB FFT Guide: Recorded Data Frequency Analysis

Published04/21/2026
Updated 07/24/2026
22 min read

Frequency analysis is useful when it leads to a decision: whether a peak is real, when it appears, and whether it should be preserved or filtered. This guide follows one recorded acceleration signal from CSV import through FFT, PSD, spectrogram, and a verified low-pass filter.

You will load a CSV, verify the sampling, calculate a correctly scaled single-sided FFT, compare windowing and PSD methods, use a spectrogram to find a component that appears partway through the record, and verify one low-pass filter. The guide focuses on uniformly sampled data. It does not cover irregular-sampling methods, acquisition hardware, production filter implementation, or physical fault diagnosis.

Every example opens in RunMat with the script and sensor data together. Run the MATLAB-compatible code, inspect the variables and plots, change a parameter, and rerun it. RunMat Desktop brings the same workflow to local files, while RunMat CLI supports repeatable runs and benchmarks.

RunMat Desktop showing a sensor CSV, frequency-analysis script, workspace variables, time trace, and single-sided spectrum

Start with the measurement

Frequency-domain analysis starts by confirming what the samples represent. The data may come from a sensor, data logger, scope, test stand, or simulation.

This guide follows one generated accelerometer export named sensor_log.csv. It contains 2,048 samples collected at 1,024 Hz. A 60 Hz component and its 180 Hz harmonic persist through the record. A 300 Hz component begins halfway through, so the spectrogram can localize its onset. Fixed noise keeps every run and screenshot repeatable.

Check the data before the FFT

Load the numeric columns with readmatrix, then calculate the sample interval from all adjacent timestamps rather than trusting only the first pair.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

sampleIntervals = diff(t);
if length(sampleIntervals) == 0
    error('The FFT requires at least two timestamped samples');
end
if any(isnan(t)) || any(isnan(x)) || any(isinf(t)) || any(isinf(x))
    error('Resolve missing or nonfinite timestamps and measurements before the FFT');
end
if any(sampleIntervals <= 0)
    error('Timestamps must increase strictly before the FFT');
end

dt = mean(sampleIntervals);
Fs = 1 / dt;
Fs = Fs(1);
timestampVariation = max(abs(sampleIntervals - dt)) / dt;
uniformityTolerance = 1e-3;
if timestampVariation > uniformityTolerance
    error('Sample times are not uniform; resample the measurement before the FFT');
end

N = length(x);
recordDuration = N / Fs;
binSpacing = Fs / N;
nyquistFrequency = Fs / 2;

fprintf('Samples: %d\n', N);
fprintf('Sample rate: %.1f Hz\n', Fs);
fprintf('Record duration: %.3f s\n', recordDuration);
fprintf('Bin spacing: %.3f Hz\n', binSpacing);
fprintf('Nyquist frequency: %.1f Hz\n', nyquistFrequency);
fprintf('Relative timestamp variation: %.3g\n', timestampVariation);

plot(t, x);
title('Sample accelerometer export');
xlabel('Time (s)');
ylabel('Acceleration (g)');
grid on;

For this file, Fs is 1,024 Hz, the record lasts 2 seconds, the FFT-bin spacing is 0.5 Hz, and Nyquist is 512 Hz.

The sample rate sets the highest representable frequency, while record duration sets the FFT-bin spacing. Resample irregular timestamps before using this frequency axis. Anti-alias filtering must happen during acquisition; an FFT cannot recover aliased content.

If the export contains missing measurements, changed columns, or irregular records, prepare and validate it before calculating the spectrum. The MATLAB CSV guide covers missing-data checks, column validation, and repeatable file import.

Compute a correctly scaled single-sided FFT

Remove the mean so the DC component does not dominate the view. Then keep an even number of samples, compute fft, and scale the positive-frequency half. Double the interior bins, but leave DC and Nyquist alone.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

Fs = 1 / mean(diff(t));
Fs = Fs(1);
if mod(length(x), 2) ~= 0
    x = x(1:end-1);
end
N = length(x);
x = x - mean(x);

Y = fft(x);
P2 = abs(Y) / N;
fFull = (0:N-1)' * Fs / N;
positiveBins = fFull <= Fs / 2;
f = fFull(positiveBins);
P1 = P2(positiveBins);
interiorBins = f > 0 & f < Fs / 2;
P1(interiorBins) = 2 * P1(interiorBins);

plot(f, P1);
title('Single-sided amplitude spectrum');
xlabel('Frequency (Hz)');
ylabel('Amplitude (g)');
grid on;
xlim([0 400]);

The 60 Hz and 180 Hz peaks represent components that remain present through the full record. The time-limited 300 Hz component also appears, but its energy is spread because it exists for only half the measurement.

The spectrum can show where energy appears. It cannot identify the physical source by itself. Tie each candidate peak back to expected shaft rates, electrical frequencies, resonances, control activity, or test events before changing the design.

Apply a window before interpreting peaks

A finite record rarely ends at exactly the same point in a cycle where it began. The discontinuity between the two ends spreads energy into neighboring bins. Tapering suppresses sidelobes but widens the main lobe.

The code below compares the unwindowed spectrum with a periodic Hann window. The windowed amplitude is divided by sum(w) to correct for coherent gain.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

Fs = 1 / mean(diff(t));
Fs = Fs(1);
if mod(length(x), 2) ~= 0
    x = x(1:end-1);
end
N = length(x);
x = x - mean(x);
fFull = (0:N-1)' * Fs / N;
positiveBins = fFull <= Fs / 2;
f = fFull(positiveBins);
interiorBins = f > 0 & f < Fs / 2;

Yrect = fft(x);
PrectFull = abs(Yrect) / N;
Prect = PrectFull(positiveBins);
Prect(interiorBins) = 2 * Prect(interiorBins);

w = hann(N, 'periodic');
Yhann = fft(x .* w);
PhannFull = abs(Yhann) / sum(w);
Phann = PhannFull(positiveBins);
Phann(interiorBins) = 2 * Phann(interiorBins);

plot(f, Prect, 'Color', [0.55 0.55 0.55], 'LineWidth', 1.5);
hold on;
plot(f, Phann, 'LineWidth', 2);
hold off;
title('Rectangular and Hann-windowed spectra');
xlabel('Frequency (Hz)');
ylabel('Amplitude (g)');
legend('Rectangular', 'Hann');
grid on;
xlim([0 400]);

Start with Hann when the record does not contain an integer number of cycles. Hamming and Blackman change the balance between main-lobe width and sidelobe suppression. Window choice changes how nearby components and weak tones beside strong ones appear, but it does not replace a longer measurement when you need finer frequency resolution.

Zero-padding adds plotted frequency samples and can make peak interpolation smoother, but record duration still sets frequency resolution.

Choose FFT magnitude, periodogram, or Welch PSD

An amplitude spectrum and a power spectral density answer different questions.

ViewUse it when you needMain tradeoff
FFT magnitudeAmplitude of discrete components in one finite recordSensitive to windowing and record boundaries
periodogramA full-record estimate of power per unit frequencyHigher estimate variance
pwelchA steadier view of the noise floor from averaged segmentsSome frequency resolution is traded for lower variance

Keep amplitude and PSD on separate axes because their units are different.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

Fs = 1 / mean(diff(t));
Fs = Fs(1);
if mod(length(x), 2) ~= 0
    x = x(1:end-1);
end
N = length(x);
x = x - mean(x);

[pPeriodogram, fPeriodogram] = periodogram(x, hann(N, 'periodic'), N, Fs);
[pWelch, fWelch] = pwelch(x, hamming(256, 'periodic'), 128, 512, Fs);

subplot(2, 1, 1);
plot(fPeriodogram, 10 * log10(pPeriodogram + 1e-12));
title('Full-record periodogram');
xlabel('Frequency (Hz)');
ylabel('PSD (dB re g^2/Hz)');
grid on;
xlim([0 400]);

subplot(2, 1, 2);
plot(fWelch, 10 * log10(pWelch + 1e-12));
title('Welch PSD estimate');
xlabel('Frequency (Hz)');
ylabel('PSD (dB re g^2/Hz)');
grid on;
xlim([0 400]);

The periodogram uses the complete record in one estimate. Welch divides the signal into overlapping, windowed segments and averages their periodograms. That averaging makes the noise-floor view less variable, but shorter segments broaden the effective frequency resolution.

Use a spectrogram when the signal changes over time

The FFT and PSD summarize a record across time. They do not show whether a component was present throughout the test or appeared during one event. A spectrogram applies short, overlapping transforms to successive sections of the signal.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

Fs = 1 / mean(diff(t));
Fs = Fs(1);
x = x - mean(x);

[s, f, tt, ps] = spectrogram(x, hamming(256, 'periodic'), 192, 512, Fs);
powerDb = 10 * log10(ps + 1e-12);

subplot(2, 1, 1);
plot(t, x);
title('Sample accelerometer export');
xlabel('Time (s)');
ylabel('Acceleration (g)');
grid on;

subplot(2, 1, 2);
imagesc([tt(1) tt(end)], [f(1) f(end)], powerDb);
axis xy;
title('Time-frequency power');
xlabel('Time (s)');
ylabel('Frequency (Hz)');
colorbar;
ylim([0 400]);

The 60 Hz and 180 Hz components remain visible across the record. The 300 Hz band appears after 1 second. A longer segment improves frequency detail but blurs when a change occurred. A shorter segment improves time localization but broadens the frequency estimate.

Interpret the result before changing the design

Before acting on a peak, check:

  • Match strong peaks to expected operating frequencies and known harmonics.
  • Treat a narrow peak differently from a raised broadband noise floor.
  • Use the spectrogram to separate persistent behavior from a startup, load change, impact, or control event.
  • Check the 0.5 Hz bin spacing before treating closely spaced peaks as distinct components.
  • Repeat the measurement or change the acquisition setup before trusting a weak feature near the noise floor.

A frequency view narrows the possible causes. Confirm them against the physical system, sensor placement, acquisition chain, and operating state.

Use the spectrum to design and verify a filter

Suppose the measurement of interest is below 200 Hz and the 300 Hz component is outside the required band. The filter now has a testable requirement: preserve the 60 Hz and 180 Hz components, attenuate the 300 Hz component, and avoid shifting events in this offline analysis.

The code below designs a fourth-order Butterworth low-pass filter with a 220 Hz cutoff. It inspects the effective response with freqz, applies zero-phase filtering with filtfilt, and compares Welch PSD estimates before and after filtering.

data = readmatrix('sensor_log.csv', 'NumHeaderLines', 1);
t = data(:, 1);
x = data(:, 2);

Fs = 1 / mean(diff(t));
Fs = Fs(1);
x = x - mean(x);

cutoffHz = 220;
filterOrder = 4;
[b, a] = butter(filterOrder, cutoffHz / (Fs / 2), 'low');
[response, responseFrequency] = freqz(b, a, 512, Fs);

xFiltered = filtfilt(b, a, x);
[pOriginal, fPsd] = pwelch(x, hamming(256, 'periodic'), 128, 512, Fs);
[pFiltered, fFiltered] = pwelch(xFiltered, hamming(256, 'periodic'), 128, 512, Fs);

effectiveResponse = abs(response).^2;

subplot(3, 1, 1);
plot(responseFrequency, 20 * log10(effectiveResponse + 1e-12));
title('Effective zero-phase filter response');
xlabel('Frequency (Hz)');
ylabel('Magnitude (dB)');
grid on;
xlim([0 400]);
ylim([-80 5]);

subplot(3, 1, 2);
plot(t, x, 'Color', [0.65 0.65 0.65]);
hold on;
plot(t, xFiltered, 'LineWidth', 1.5);
hold off;
title('Signal before and after filtering');
xlabel('Time (s)');
ylabel('Acceleration (g)');
legend('Original', 'Filtered');
grid on;

subplot(3, 1, 3);
plot(fPsd, 10 * log10(pOriginal + 1e-12), 'Color', [0.65 0.65 0.65]);
hold on;
plot(fFiltered, 10 * log10(pFiltered + 1e-12), 'LineWidth', 1.5);
hold off;
title('PSD before and after filtering');
xlabel('Frequency (Hz)');
ylabel('PSD (dB re g^2/Hz)');
legend('Original', 'Filtered');
grid on;
xlim([0 400]);

The response and PSD plots verify that the 300 Hz component is attenuated while the 60 Hz and 180 Hz components remain. The time trace checks whether filtering changed the waveform in an unexpected way.

filtfilt runs the filter forward and backward. The phase delay cancels, while the effective magnitude response becomes the square of the single-pass response. That behavior is useful for offline analysis, but it is not a causal real-time implementation. Use filter for causal processing or fir1 when a linear-phase FIR design is a better fit.

Deleting FFT bins creates an abrupt frequency mask, which can introduce ringing. A finite-record FFT also assumes periodic boundaries. A designed filter makes the passband, stopband, and phase behavior explicit; long-record FFT filtering with overlap-add or overlap-save is a separate implementation problem.

Run the same analysis on your own data

The examples on this page open in the browser with the script and CSV together. The complete workspace also includes frequency_analysis.m, which produces the time trace, amplitude spectrum, Welch PSD, spectrogram, and low-pass filter comparison from the same file.

Open local measurement files in RunMat Desktop, rerun the script, and inspect the variables and plots after each change. Project history keeps the analysis reviewable, while RunMat CLI repeats the same script in benchmarks or automated checks. Desktop also uses local hardware and supported GPU paths.

For multi-file analysis, see the RunMat Projects documentation.

Download RunMat Desktop

Use ifft to reconstruct a time-domain signal after modifying its spectrum. See the fft2, fftshift, and ifftshift references for multidimensional transforms and shifted spectra. For plotting syntax, see the MATLAB plotting guide.

Frequently asked questions

How do I calculate a single-sided FFT in MATLAB or RunMat?⌄

Remove the signal mean, compute Y = fft(x), divide the magnitude by the sample count, retain bins from DC through Nyquist, and double only the interior positive-frequency bins. Do not double the DC or Nyquist bins.

How do I build the frequency axis for an FFT?⌄

For an even-length signal with N samples at sample rate Fs, use f = (0:N/2)' * Fs / N for the single-sided spectrum. The bin spacing is Fs/N and the highest represented frequency is the Nyquist frequency Fs/2.

What is the difference between FFT magnitude and PSD?⌄

An FFT magnitude spectrum shows component amplitudes for one finite record. A power spectral density estimate describes how signal power is distributed per unit frequency, which is often more useful for noise and broadband measurements.

When should I use pwelch instead of a periodogram?⌄

Use a periodogram when you want a full-record PSD estimate with the record's available frequency resolution. Use pwelch when averaging overlapping windowed segments is a useful trade for a lower-variance estimate of the noise floor.

Which window should I use for FFT analysis?⌄

Start with Hann when the record does not contain an integer number of cycles. Hamming and Blackman change the balance between main-lobe width and sidelobe suppression. Apply coherent-gain correction before interpreting tone amplitude.

Does zero-padding improve FFT resolution?⌄

Zero-padding adds plotted frequency samples and can make peak interpolation smoother, but record duration still sets frequency resolution.

What should I do if the sample times are not uniform?⌄

Do not build a conventional FFT frequency axis from irregular timestamps. Confirm the acquisition timing and resample the measurement onto a uniform time grid before using the standard FFT workflow.

How do I remove high-frequency noise after an FFT?⌄

Use the spectrum to define the passband and stopband, design a suitable low-pass filter, inspect its response, and compare the signal and PSD before and after filtering. The worked example uses butter, freqz, filtfilt, and pwelch.

Should I filter a signal by setting FFT bins to zero?⌄

Usually not as a first choice. An abrupt frequency mask can introduce ringing, and a finite-record FFT assumes periodic boundaries. A designed filter makes the passband, stopband, and phase behavior explicit.

When should I use filtfilt instead of filter?⌄

Use filtfilt for offline analysis when phase delay would make event timing harder to interpret. Use filter for causal or real-time processing because filtfilt requires the complete record and runs the filter forward and backward.

Can I run the CSV FFT example in the browser?⌄

Yes. Every runnable example on this guide opens in the RunMat browser sandbox with sensor_log.csv already in the workspace.

Does RunMat run FFT calculations on the GPU?⌄

When the active GPU provider implements FFT, RunMat uses it. Otherwise, RunMat transfers the input to CPU and computes the FFT there.

How does RunMat FFT compare with MATLAB FFT?⌄

RunMat supports MATLAB-compatible fft call forms for transform length and dimension, plus the related ifft, fft2, ifft2, fftshift, and ifftshift functions. RunMat is an independent runtime, so validate the signatures and numerical behavior your project depends on.

Enjoyed this post? Join the newsletter

Monthly updates on RunMat internals, development, and performance tips.

Download RunMat

Download RunMat for full performance, or use RunMat in your browser for zero setup.