WingbeatFrequencyTracker.java
package org.hammer.audio.experimental.acoustic;
import org.hammer.audio.analysis.Fft;
import org.hammer.audio.core.AudioBlock;
/** Experimental narrow-band frequency tracker for wingbeat-like tonal sources. */
public final class WingbeatFrequencyTracker {
private final int fftSize;
private final FrequencyBand searchBand;
private final Fft fft;
/** Create a tracker with a power-of-two FFT size and a search band. */
public WingbeatFrequencyTracker(int fftSize, FrequencyBand searchBand) {
this.fftSize = fftSize;
this.searchBand = searchBand;
this.fft = new Fft(fftSize);
}
/** Track the strongest peak in {@code channel}. */
public SpectralPeak track(AudioBlock block, int channel) {
float[] samples = block.channelView(channel);
float[] re = new float[fftSize];
float[] im = new float[fftSize];
int copied = Math.min(samples.length, fftSize);
System.arraycopy(samples, 0, re, 0, copied);
applyHannWindow(re, copied);
fft.forward(re, im);
float[] magnitudes = new float[fftSize / 2 + 1];
fft.magnitudesOneSided(re, im, magnitudes);
int lowBin =
Math.max(0, (int) Math.ceil(searchBand.lowHz() * fftSize / block.format().sampleRate()));
int highBin =
Math.min(
magnitudes.length - 1,
(int) Math.floor(searchBand.highHz() * fftSize / block.format().sampleRate()));
int bestBin = lowBin;
double bestMagnitude = 0.0;
double bandEnergy = 0.0;
for (int bin = lowBin; bin <= highBin; bin++) {
double magnitude = magnitudes[bin];
bandEnergy += magnitude;
if (magnitude > bestMagnitude) {
bestMagnitude = magnitude;
bestBin = bin;
}
}
double frequency = bestBin * block.format().sampleRate() / fftSize;
double confidence = bandEnergy > 0.0 ? Math.min(1.0, bestMagnitude / bandEnergy) : 0.0;
return new SpectralPeak(frequency, bestMagnitude, confidence);
}
private static void applyHannWindow(float[] samples, int frames) {
if (frames <= 1) {
return;
}
for (int i = 0; i < frames; i++) {
samples[i] *= (float) (0.5 - 0.5 * Math.cos(2.0 * Math.PI * i / (frames - 1)));
}
}
}