SpectrumAnalyzer.java
package org.hammer.audio.analysis;
import org.hammer.audio.core.AudioBlock;
/**
* FFT-based spectrum analyzer.
*
* <p>Takes one channel of an {@link AudioBlock}, applies a Hann window of the configured FFT size,
* computes the forward FFT and produces a {@link SpectrumSnapshot} containing the one-sided
* magnitude spectrum.
*
* <p>If the input block contains fewer frames than the FFT size, the remaining samples are
* zero-padded. If it contains more, only the first {@code fftSize} frames are analyzed.
*
* <p>Internally this analyzer caches per-instance scratch buffers; instances are <strong>not
* thread-safe</strong>. Create one per analysis thread or guard externally.
*
* @author refactoring
*/
public final class SpectrumAnalyzer implements AnalysisModule<SpectrumSnapshot> {
private final int fftSize;
private final int channel;
private final float sampleRate;
private final Fft fft;
private final float[] window;
private final float[] re;
private final float[] im;
private final float[] magnitudes;
/**
* Create a new spectrum analyzer.
*
* @param fftSize FFT size; must be a power of two and {@code >= 2}
* @param channel channel index of the source block to analyze (0 for mono)
* @param sampleRate sample rate of the source audio in Hz
* @throws IllegalArgumentException if any parameter is invalid
*/
public SpectrumAnalyzer(int fftSize, int channel, float sampleRate) {
if (channel < 0) {
throw new IllegalArgumentException("channel must be >= 0, was " + channel);
}
if (!(sampleRate > 0f)) {
throw new IllegalArgumentException("sampleRate must be > 0, was " + sampleRate);
}
this.fftSize = fftSize;
this.channel = channel;
this.sampleRate = sampleRate;
this.fft = new Fft(fftSize);
this.window = hannWindow(fftSize);
this.re = new float[fftSize];
this.im = new float[fftSize];
this.magnitudes = new float[fftSize / 2 + 1];
}
@Override
public SpectrumSnapshot analyze(AudioBlock block) {
float[] samples = block.channelView(channel);
int n = Math.min(samples.length, fftSize);
// Apply Hann window and zero-pad remainder.
for (int i = 0; i < n; i++) {
re[i] = samples[i] * window[i];
im[i] = 0f;
}
for (int i = n; i < fftSize; i++) {
re[i] = 0f;
im[i] = 0f;
}
fft.forward(re, im);
fft.magnitudes(re, im, magnitudes);
return new SpectrumSnapshot(
block.frameIndex(), block.timestampNanos(), channel, sampleRate, fftSize, magnitudes);
}
/**
* @return the configured FFT size
*/
public int fftSize() {
return fftSize;
}
/** Compute a Hann window of length {@code n}. */
private static float[] hannWindow(int n) {
float[] w = new float[n];
if (n <= 1) {
if (n == 1) {
w[0] = 1f;
}
return w;
}
double scale = 2.0 * Math.PI / (n - 1);
for (int i = 0; i < n; i++) {
w[i] = (float) (0.5 * (1.0 - Math.cos(scale * i)));
}
return w;
}
}