FrequencyTrack.java
package org.hammer.audio.experimental.acoustic.doppler;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/** Stabilizes a source-frequency estimate over a bounded history of frames. */
public final class FrequencyTrack {
private final int maxHistory;
private final double smoothingAlpha;
private final Deque<Double> history = new ArrayDeque<>();
private double stableFrequency;
/** Create a frequency stabilizer. */
public FrequencyTrack(int maxHistory, double smoothingAlpha) {
if (maxHistory < 1) {
throw new IllegalArgumentException("maxHistory must be >= 1");
}
if (!Double.isFinite(smoothingAlpha) || smoothingAlpha <= 0.0 || smoothingAlpha > 1.0) {
throw new IllegalArgumentException("smoothingAlpha must be in (0,1]");
}
this.maxHistory = maxHistory;
this.smoothingAlpha = smoothingAlpha;
}
/** Add one observed frequency and return the updated stable frequency. */
public double update(double observedFrequencyHz) {
if (!Double.isFinite(observedFrequencyHz) || observedFrequencyHz < 0.0) {
throw new IllegalArgumentException("observedFrequencyHz must be finite and >= 0");
}
if (history.isEmpty()) {
stableFrequency = observedFrequencyHz;
} else {
stableFrequency =
smoothingAlpha * observedFrequencyHz + (1.0 - smoothingAlpha) * stableFrequency;
}
history.addLast(observedFrequencyHz);
while (history.size() > maxHistory) {
history.removeFirst();
}
return stableFrequency;
}
/** Current reference frequency in Hz. */
public double stableFrequency() {
return stableFrequency;
}
/** Immutable history of observed frequencies in Hz. */
public List<Double> history() {
return List.copyOf(history);
}
/** Sample variance of the bounded frequency history. */
public double variance() {
if (history.size() < 2) {
return 0.0;
}
double mean = 0.0;
for (double value : history) {
mean += value;
}
mean /= history.size();
double sumSquares = 0.0;
for (double value : history) {
double diff = value - mean;
sumSquares += diff * diff;
}
return sumSquares / (history.size() - 1);
}
/** Snapshot copy preserving the stable frequency. */
public FrequencyTrackSnapshot snapshot() {
return new FrequencyTrackSnapshot(stableFrequency, new ArrayList<>(history), variance());
}
/** Immutable frequency-track state for metrics and visualization. */
public record FrequencyTrackSnapshot(
double stableFrequency, List<Double> history, double variance) {
/** Validate and copy fields. */
public FrequencyTrackSnapshot {
if (!Double.isFinite(stableFrequency) || stableFrequency < 0.0) {
throw new IllegalArgumentException("stableFrequency must be finite and >= 0");
}
history = List.copyOf(history);
if (!Double.isFinite(variance) || variance < 0.0) {
throw new IllegalArgumentException("variance must be finite and >= 0");
}
}
}
}