SpectrogramAnalyzer.java
package org.hammer.audio.spectrogram;
import org.hammer.audio.analysis.AnalysisModule;
import org.hammer.audio.analysis.SpectrumAnalyzer;
import org.hammer.audio.analysis.SpectrumSnapshot;
import org.hammer.audio.core.AudioBlock;
/**
* Analyzer that computes a {@link SpectrogramFrame} per {@link AudioBlock} and appends it to an
* internal {@link SpectrogramHistory}.
*
* <p>Implemented on top of {@link SpectrumAnalyzer}; the per-block FFT result is also exposed as a
* {@link SpectrumSnapshot} via {@link #lastSpectrum()} for callers that need both views without
* paying the cost of a second FFT.
*
* <p>Instances are <strong>not thread-safe</strong> and are intended to be driven by a single
* analysis / UI thread.
*/
public final class SpectrogramAnalyzer implements AnalysisModule<SpectrogramFrame> {
private final SpectrumAnalyzer spectrumAnalyzer;
private final SpectrogramHistory history;
private SpectrumSnapshot lastSpectrum;
/**
* Create a spectrogram 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
* @param historyFrames maximum number of retained frames; must be {@code >= 1}
*/
public SpectrogramAnalyzer(int fftSize, int channel, float sampleRate, int historyFrames) {
this(new SpectrumAnalyzer(fftSize, channel, sampleRate), new SpectrogramHistory(historyFrames));
}
/**
* Create a spectrogram analyzer with externally supplied dependencies.
*
* @param spectrumAnalyzer FFT-based spectrum analyzer
* @param history rolling history of frames
*/
public SpectrogramAnalyzer(SpectrumAnalyzer spectrumAnalyzer, SpectrogramHistory history) {
this.spectrumAnalyzer = spectrumAnalyzer;
this.history = history;
}
@Override
public SpectrogramFrame analyze(AudioBlock block) {
SpectrumSnapshot snapshot = spectrumAnalyzer.analyze(block);
// Use the non-copying view + adopting factory to avoid an extra defensive clone in the
// per-block hot path (the SpectrogramFrame still ends up with its own backing array).
SpectrogramFrame frame =
SpectrogramFrame.adopting(
snapshot.sourceFrameIndex(),
snapshot.sourceTimestampNanos(),
snapshot.sampleRate(),
snapshot.fftSize(),
snapshot.magnitudesView());
history.append(frame);
lastSpectrum = snapshot;
return frame;
}
/**
* @return the spectrogram history backing this analyzer
*/
public SpectrogramHistory history() {
return history;
}
/**
* @return the most recent {@link SpectrumSnapshot}, or {@code null} if {@link
* #analyze(AudioBlock)} has not yet been called
*/
public SpectrumSnapshot lastSpectrum() {
return lastSpectrum;
}
/**
* @return configured FFT size
*/
public int fftSize() {
return spectrumAnalyzer.fftSize();
}
}