Skip to content

Commit

Permalink
Apply Hann window to FFT analyzer and improve documentation (closes #77)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinstadler committed Oct 3, 2023
1 parent 4c318db commit 63ed663
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 29 deletions.
86 changes: 57 additions & 29 deletions src/processing/sound/FFT.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,32 @@ public FFT(PApplet parent) {
* @param parent
* typically use "this"
* @param bands
* number of frequency bands for the FFT as an integer. This
* parameter needs to be a power of 2 (e.g. 16, 32, 64, 128,
* ...). The default is 512.
* number of frequency bands for the FFT. This parameter needs to
* be a power of 2 (e.g. 16, 32, 64, 128, ...). The default is 512.
*/
public FFT(PApplet parent, int bands) {
super(parent);
if (bands < 0 || Integer.bitCount(bands) != 1) {
// TODO throw RuntimeException?
Engine.printError("number of FFT bands needs to be a power of 2");
} else {
// FFT buffer size is twice the number of frequency bands
if (FFT.checkNumBands(bands)) {
// if we want to be able to detect something for the highest band, the FFT
// buffer size needs to be twice the number of frequency bands
this.fft = new JSynFFT(2 * bands);
this.spectrum = new float[bands];
}
}

private static boolean checkNumBands(int bands) {
// just print an error message instead of interrupting the whole sketch
// executing with a RuntimeException
if (bands < 0 || Integer.bitCount(bands) != 1) {
Engine.printError("number of FFT bands needs to be a power of 2");
return false;
} else if (bands > 16384) {
Engine.printError("the maximum number of FFT bands is 16384");
return false;
}
return true;
}

protected void removeInput() {
this.fft.input.disconnectAll();
this.input = null;
Expand All @@ -60,43 +70,55 @@ public float[] analyze() {
}

/**
* Calculates the current frequency spectrum of the input source and returns
* it as an array with as many elements as frequency bands.
* Calculates the current frequency spectrum of the input signal.
* Returns an array with as many elements as this FFT analyzer's number of
* frequency bands. The frequency associated with each band of the spectrum is
* <code>frequency = binIndex * sampleRate / (2*numBands)</code>.<br>
*
* @param value
* an array with as many elements as this FFT analyzer's number of
* frequency bands
* @return The current frequency spectrum of the input source. The array has as
* many elements as this FFT analyzer's number of frequency bands.
* The values of the resulting array show the amplitudes of pure tone
* components contained in the signal. If the signal is a sine with an
* amplitude of 1, the spectrum will have an absolute value of 1 (0 dB) at the
* frequency of the sine. For complex real-world signals the spectrum values
* will be much lower and usually don't exceed 0.05.
* @param target
* if provided, writes the frequency spectrum into the given array.
* The array needs to have as many elements as this FFT analyzer's
* number of frequency bands.
* @webref Analysis:FFT
* @webBrief Calculates the current frequency spectrum of the audio input
* signal.
**/
public float[] analyze(float[] value) {
public float[] analyze(float[] target) {
if (this.input == null) {
Engine.printWarning("this FFT has no sound source connected to it, nothing to analyze");
}
this.fft.calculateMagnitudes(value);
return value;
this.fft.calculateMagnitudes(target);
return target;
}

/**
* Calculates the frequency spectrum of a given audio sample and returns an
* array of magnitudes, one for each frequency band.
*
* array of magnitudes, one for each frequency band. The frequency associated
* with each band of the spectrum is <code>frequency = binIndex * sampleRate /
* (2*numBands)</code>.<br>
* This version is intended to be used in non-real time processing, particularly when you are
* creating an animation in non-real time and want to get the FFT for a particular chunk of an audio sample.
*
* For stereo samples, you can call this function once for each channel, so you can display the left and right
* fft values separately.
* fft values separately.<br>
*
* The values of the resulting array show the amplitudes of pure tone
* components contained in the signal. If the signal is a sine with an
* amplitude of 1, the spectrum will have an absolute value of 1 (0 dB) at the
* frequency of the sine. For complex real-world signals the spectrum values
* will be much lower and usually don't exceed 0.05.
* @param sample
* an array with sound samples
* an array of numbers that describe the waveform to be analyzed
* @param numBands
* the number of fft bands requested. Must be a power of 2 (one of 2, 4, 8, 16 etc.)
* @param target float array that the computed spectrum will be written into.
* The FFT will compute as many frequency bands as the length of this array,
* which must be a power of 2 (2, 4, 8, 16 etc.)
* @param target array that the computed spectrum will be written to. The FFT
* will compute as many frequency bands as the length of this array, which
* must be a power of 2 (2, 4, 8, 16 etc.)
* @return The frequency spectrum of the given audio sample. The array has as
* many elements as this FFT analyzer's number of frequency bands.
* @webref Analysis:FFT
Expand All @@ -113,11 +135,17 @@ public static float[] analyzeSample(float[] sample, int numBands) {

// the meat of the matter
protected static void calculateMagnitudesFromSample(float[] sample, float[] imaginary, float[] target) {
if (Integer.bitCount(target.length) != 1) {
Engine.printError("number of FFT bands needs to be a power of 2");
if (FFT.checkNumBands(target.length)) {
FourierMath.transform(1, sample.length, sample, imaginary);
FourierMath.calculateMagnitudes(sample, imaginary, target);
// there is an argument for multiplying the normalized spectrum amplitude
// values by two, see e.g.:
// https://pyfar.readthedocs.io/en/stable/concepts/pyfar.fft.html#fft-normalizations
// https://de.mathworks.com/matlabcentral/answers/162846-amplitude-of-signal-after-fft-operation#answer_159088
for (int i = 0; i < target.length; i++) {
target[i] *= 2;
}
}
FourierMath.transform(1, sample.length, sample, imaginary);
FourierMath.calculateMagnitudes(sample, imaginary, target);
}

protected static void calculateMagnitudesFromSample(float[] sample, float[] target) {
Expand Down
11 changes: 11 additions & 0 deletions src/processing/sound/JSynFFT.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.Arrays;

import com.jsyn.data.FloatSample;
import com.jsyn.data.HannWindow;
import com.jsyn.data.SpectralWindow;
import com.jsyn.unitgen.FixedRateMonoWriter;

/**
Expand All @@ -15,11 +17,14 @@ class JSynFFT extends FixedRateMonoWriter {
private float[] real;
private float[] imaginary;

private SpectralWindow window;

protected JSynFFT(int bufferSize) {
super();
this.buffer = new FloatSample(bufferSize);
this.real = new float[bufferSize];
this.imaginary = new float[bufferSize];
this.window = new HannWindow(bufferSize);

// write any connected input into the output buffer ad infinitum
this.dataQueue.queueLoop(this.buffer);
Expand All @@ -30,6 +35,12 @@ protected void calculateMagnitudes(float[] target) {
int pos = (int) this.dataQueue.getFrameCount() % this.buffer.getNumFrames();
this.buffer.read(pos, this.real, 0, this.buffer.getNumFrames() - pos);
this.buffer.read(0, this.real, this.buffer.getNumFrames() - pos, pos);
if (this.window != null) {
for (int i = 0; i < this.real.length; i++) {
this.real[i] *= this.window.get(i);
}
}
Arrays.fill(this.imaginary, 0);
FFT.calculateMagnitudesFromSample(this.real, this.imaginary, target);
}

Expand Down

0 comments on commit 63ed663

Please sign in to comment.