SpectrogramFrame.java
package org.hammer.audio.spectrogram;
import java.util.Objects;
import org.hammer.audio.analysis.AnalysisSnapshot;
/**
* Immutable single frame of a spectrogram / waterfall: the one-sided magnitude spectrum captured at
* a specific {@link #sourceTimestampNanos()} / {@link #sourceFrameIndex()}, together with the
* sample-rate and FFT size required to interpret it.
*
* <p>The magnitude array is defensively copied on construction. {@link #magnitudes()} returns a
* defensive copy; {@link #magnitudesView()} returns the internal array directly for hot rendering
* paths and must not be mutated.
*/
public final class SpectrogramFrame implements AnalysisSnapshot {
private final long sourceFrameIndex;
private final long sourceTimestampNanos;
private final float sampleRate;
private final int fftSize;
private final float[] magnitudes;
/**
* Create a new spectrogram frame. The magnitude array is defensively copied.
*
* @param sourceFrameIndex frame index of the originating audio block
* @param sourceTimestampNanos timestamp of the originating audio block in nanoseconds
* @param sampleRate sample rate of the source audio in Hz; must be {@code > 0}
* @param fftSize FFT size that produced the magnitudes; must be a positive even number
* @param magnitudes one-sided magnitude spectrum of length {@code fftSize/2 + 1}; must not be
* {@code null}
* @throws IllegalArgumentException if any parameter is invalid
*/
public SpectrogramFrame(
long sourceFrameIndex,
long sourceTimestampNanos,
float sampleRate,
int fftSize,
float[] magnitudes) {
if (!(sampleRate > 0f)) {
throw new IllegalArgumentException("sampleRate must be > 0, was " + sampleRate);
}
if (fftSize < 2 || (fftSize & 1) != 0) {
throw new IllegalArgumentException("fftSize must be a positive even number, was " + fftSize);
}
Objects.requireNonNull(magnitudes, "magnitudes");
int expected = fftSize / 2 + 1;
if (magnitudes.length != expected) {
throw new IllegalArgumentException(
"magnitudes.length must be fftSize/2+1=" + expected + ", was " + magnitudes.length);
}
this.sourceFrameIndex = sourceFrameIndex;
this.sourceTimestampNanos = sourceTimestampNanos;
this.sampleRate = sampleRate;
this.fftSize = fftSize;
this.magnitudes = magnitudes.clone();
}
/**
* Internal constructor used by {@link org.hammer.audio.spectrogram.SpectrogramAnalyzer} to adopt
* an already-validated magnitudes array without copying. The supplied array becomes the frame's
* backing buffer and must not be mutated by the caller after the call.
*/
static SpectrogramFrame adopting(
long sourceFrameIndex,
long sourceTimestampNanos,
float sampleRate,
int fftSize,
float[] magnitudes) {
SpectrogramFrame frame =
new SpectrogramFrame(sourceFrameIndex, sourceTimestampNanos, sampleRate, fftSize);
frame.adoptMagnitudes(magnitudes);
return frame;
}
private SpectrogramFrame(
long sourceFrameIndex, long sourceTimestampNanos, float sampleRate, int fftSize) {
this.sourceFrameIndex = sourceFrameIndex;
this.sourceTimestampNanos = sourceTimestampNanos;
this.sampleRate = sampleRate;
this.fftSize = fftSize;
this.magnitudes = new float[fftSize / 2 + 1];
}
private void adoptMagnitudes(float[] source) {
// The constructor has already allocated the backing array; a single arraycopy here avoids
// the second full-array allocation that the public constructor's defensive clone would do
// on top of the clone SpectrumSnapshot already performed.
System.arraycopy(source, 0, this.magnitudes, 0, this.magnitudes.length);
}
/**
* @return frame index of the originating audio block
*/
@Override
public long sourceFrameIndex() {
return sourceFrameIndex;
}
/**
* @return timestamp of the originating audio block in nanoseconds
*/
@Override
public long sourceTimestampNanos() {
return sourceTimestampNanos;
}
/**
* @return sample rate of the source audio in Hz
*/
public float sampleRate() {
return sampleRate;
}
/**
* @return FFT size that produced the magnitudes
*/
public int fftSize() {
return fftSize;
}
/**
* @return number of one-sided frequency bins (DC ... Nyquist)
*/
public int binCount() {
return magnitudes.length;
}
/**
* @return bin width in Hz, i.e. {@code sampleRate / fftSize}
*/
public float binWidthHz() {
return sampleRate / fftSize;
}
/**
* @param bin bin index in {@code [0, binCount())}
* @return centre frequency of the bin in Hz
*/
public float frequencyOfBin(int bin) {
return bin * binWidthHz();
}
/**
* @param bin bin index in {@code [0, binCount())}
* @return magnitude at the bin
*/
public float magnitude(int bin) {
return magnitudes[bin];
}
/**
* @return defensive copy of the magnitude spectrum
*/
public float[] magnitudes() {
return magnitudes.clone();
}
/**
* Read-only access to the internal magnitudes array. Callers must not mutate the returned array.
* Intended for hot rendering paths that need to avoid per-frame allocations.
*
* @return the internal magnitudes array (do not mutate)
*/
public float[] magnitudesView() {
return magnitudes;
}
}