DocImageRenderer.java
package org.hammer.tools;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import javax.imageio.ImageIO;
import org.hammer.audio.analysis.MeasurementCalculator;
import org.hammer.audio.analysis.MeasurementSnapshot;
import org.hammer.audio.analysis.PeakHoldSpectrum;
import org.hammer.audio.analysis.SpectrumAnalyzer;
import org.hammer.audio.analysis.SpectrumAverager;
import org.hammer.audio.analysis.SpectrumSnapshot;
import org.hammer.audio.analysis.WaveformTrigger;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
import org.hammer.audio.diagnosis.DiagnosisAnalyzer;
import org.hammer.audio.diagnosis.DiagnosisFinding;
import org.hammer.audio.diagnosis.DiagnosisSnapshot;
import org.hammer.audio.signal.SineGenerator;
import org.hammer.audio.signal.SquareGenerator;
import org.hammer.audio.spectrogram.SpectrogramAnalyzer;
import org.hammer.audio.spectrogram.SpectrogramHistory;
import org.hammer.audio.ui.theme.PlotRenderTheme;
/**
* Headless utility that renders deterministic PNG screenshots used in the README and feature
* documentation.
*
* <p>Re-run with:
*
* <pre>
* ./mvnw -pl audio-app -am package -DskipTests
* java -cp "audio-app/target/audio-app-0.0.1-SNAPSHOT.jar:audio-app/target/lib/*" \
* org.hammer.tools.DocImageRenderer docs/images
* </pre>
*
* <p>The output directory defaults to {@code docs/images} when no argument is given. The README
* screenshot is written to {@code screenshot.png}; feature images are written to the {@code
* features/} child directory.
*/
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class DocImageRenderer {
private static final int W = 760;
private static final int H = 320;
private static final int DASHBOARD_W = 1600;
private static final int DASHBOARD_H = 1000;
private static final int DASHBOARD_SPECTROGRAM_HISTORY_FRAMES = 180;
private static final int DASHBOARD_SPECTROGRAM_SEED_BLOCKS = 64;
private static final int DASHBOARD_WAVEFORM_VISIBLE_SAMPLES = 2200;
private static final float SYNTHETIC_SPECTROGRAM_PULSE_BASE = 0.45f;
private static final float SYNTHETIC_SPECTROGRAM_PULSE_SWING = 0.55f;
private static final double SYNTHETIC_SPECTROGRAM_PULSE_PERIOD = 18.0;
private static final float SYNTHETIC_SPECTROGRAM_BAND_POSITION = 0.72f;
private static final float SYNTHETIC_SPECTROGRAM_BAND_WIDTH = 18f;
private static final AudioFormatDescriptor MONO_44K = new AudioFormatDescriptor(44100f, 1, 16);
private static final int FFT = 1024;
private DocImageRenderer() {}
/**
* @param args optional output directory; defaults to {@code docs/images/features}
* @throws IOException if any of the PNGs cannot be written
*/
public static void main(String[] args) throws IOException {
Path imageDir = Path.of(args.length > 0 ? args[0] : "docs/images");
Path featureDir = imageDir.resolve("features");
Files.createDirectories(featureDir);
writePng(imageDir.resolve("screenshot.png"), renderDashboardScreenshot());
writePng(featureDir.resolve("waveform-trigger.png"), renderTrigger());
writePng(featureDir.resolve("spectrum-peak-hold.png"), renderSpectrumPeakHold());
writePng(featureDir.resolve("recording-format.png"), renderRecordingFormat());
writePng(featureDir.resolve("ab-comparison.png"), renderAbComparison());
}
/**
* Render the deterministic README dashboard screenshot.
*
* @return a 1600x1000 PNG-ready image showing a 440 Hz demo signal and the main dashboard panels
*/
public static BufferedImage renderDashboardScreenshot() {
SineGenerator gen = new SineGenerator(MONO_44K, 440.0, 0.7f);
AudioBlock block = gen.nextBlock(4096);
SpectrumAnalyzer spectrumAnalyzer = new SpectrumAnalyzer(FFT, 0, MONO_44K.sampleRate());
SpectrumSnapshot spectrum = spectrumAnalyzer.analyze(block);
MeasurementSnapshot measurement = new MeasurementCalculator().calculate(block, spectrum);
SpectrogramAnalyzer spectrogramAnalyzer =
new SpectrogramAnalyzer(
FFT, 0, MONO_44K.sampleRate(), DASHBOARD_SPECTROGRAM_HISTORY_FRAMES);
for (int i = 0; i < DASHBOARD_SPECTROGRAM_SEED_BLOCKS; i++) {
spectrogramAnalyzer.analyze(gen.nextBlock(FFT));
}
SpectrogramHistory history = spectrogramAnalyzer.history();
DiagnosisSnapshot diagnosis = new DiagnosisAnalyzer().analyze(block, spectrum, history, null);
BufferedImage img = new BufferedImage(DASHBOARD_W, DASHBOARD_H, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
try {
applyHints(g);
g.setColor(new Color(19, 24, 32));
g.fillRect(0, 0, DASHBOARD_W, DASHBOARD_H);
drawAppChrome(g);
drawControlBar(g, measurement, spectrum);
drawWaveform(g, new Rectangle(28, 150, 1544, 330), block);
drawSpectrumPanel(
g,
new Rectangle(28, 508, 754, 245),
spectrum,
"Spectrum — 440 Hz sine demo",
PlotRenderTheme.SPECTRUM_LINE);
drawMeasurements(g, new Rectangle(810, 508, 762, 245), measurement);
drawSpectrogram(g, new Rectangle(28, 782, 1050, 180));
drawDiagnosis(g, new Rectangle(1104, 782, 468, 180), diagnosis);
} finally {
g.dispose();
}
return img;
}
private static void drawAppChrome(Graphics2D g) {
g.setColor(new Color(31, 38, 48));
g.fillRect(0, 0, DASHBOARD_W, 56);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 18));
g.drawString("Audio Analyzer", 24, 35);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.setColor(PlotRenderTheme.TEXT_MUTED);
g.drawString("File View Plugins Help", 210, 35);
g.setColor(new Color(52, 168, 83));
g.fillRoundRect(DASHBOARD_W - 170, 16, 132, 24, 12, 12);
g.setColor(Color.WHITE);
g.drawString("Demo frozen", DASHBOARD_W - 148, 33);
}
private static void drawControlBar(
Graphics2D g, MeasurementSnapshot measurement, SpectrumSnapshot spectrum) {
Rectangle controls = new Rectangle(28, 76, 1544, 50);
g.setColor(new Color(35, 43, 55));
g.fillRoundRect(controls.x, controls.y, controls.width, controls.height, 14, 14);
g.setColor(new Color(72, 84, 102));
g.drawRoundRect(controls.x, controls.y, controls.width, controls.height, 14, 14);
String[] items = {
"Input: Demo mode",
"Demo: Sine",
"Format: 44.1 kHz / mono / 16-bit",
String.format(Locale.ROOT, "Peak: %.1f Hz", strongestFrequency(spectrum)),
String.format(Locale.ROOT, "RMS: %.3f", measurement.rms()),
String.format(Locale.ROOT, "Level: %.2f", measurement.peakLevel())
};
g.setFont(PlotRenderTheme.LABEL_FONT);
int x = controls.x + 18;
for (String item : items) {
drawPill(g, x, controls.y + 12, item);
x += g.getFontMetrics().stringWidth(item) + 42;
}
}
private static void drawPill(Graphics2D g, int x, int y, String text) {
int width = g.getFontMetrics().stringWidth(text) + 20;
g.setColor(new Color(47, 58, 74));
g.fillRoundRect(x, y, width, 26, 13, 13);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.drawString(text, x + 10, y + 18);
}
private static void drawWaveform(Graphics2D g, Rectangle plot, AudioBlock block) {
PlotRenderTheme.drawPlotBackground(g, plot.width, plot.height, plot);
PlotRenderTheme.drawGrid(g, plot, 16, 8);
PlotRenderTheme.drawTitle(g, plot.x + 12, plot.y + 22, "Waveform — reproducible 440 Hz sine");
PlotRenderTheme.drawYAxisLabel(g, plot, "Amplitude [-1..1]");
PlotRenderTheme.drawYTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"+1", "0", "-1"});
PlotRenderTheme.drawXAxisLabel(g, plot, "Time [ms]");
float[] samples = block.channelView(0);
int visible = Math.min(samples.length, DASHBOARD_WAVEFORM_VISIBLE_SAMPLES);
double durationMs = 1000.0d * Math.max(0, visible - 1) / MONO_44K.sampleRate();
PlotRenderTheme.drawXTicks(
g,
plot,
new double[] {0.0d, 0.5d, 1.0d},
new String[] {
"0 ms",
String.format(Locale.ROOT, "%.1f ms", durationMs / 2.0d),
String.format(Locale.ROOT, "%.1f ms", durationMs)
});
int centerY = plot.y + plot.height / 2;
int amplitude = plot.height / 2 - 38;
Path2D path = new Path2D.Float();
for (int i = 0; i < visible; i++) {
double x = plot.x + (double) i * (plot.width - 1) / Math.max(1, visible - 1);
double y = centerY - Math.max(-1f, Math.min(1f, samples[i])) * amplitude;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
g.setColor(PlotRenderTheme.CENTER_LINE);
g.drawLine(plot.x, centerY, plot.x + plot.width, centerY);
g.setColor(PlotRenderTheme.WAVEFORM_LEFT);
g.setStroke(PlotRenderTheme.TRACE_STROKE);
g.draw(path);
g.setColor(PlotRenderTheme.TEXT_MUTED);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.drawString("Frozen demo buffer, amplitude 0.70, no clipping", plot.x + 12, plot.y + 44);
}
private static void drawMeasurements(
Graphics2D g, Rectangle panel, MeasurementSnapshot measurement) {
drawPanelShell(g, panel, "Measurements");
String[] rows = {
String.format(
Locale.ROOT, "Dominant frequency %.1f Hz", measurement.dominantFrequencyHz()),
String.format(Locale.ROOT, "RMS level %.3f", measurement.rms()),
String.format(Locale.ROOT, "Peak level %.3f", measurement.peakLevel()),
"Clipping no",
"Stereo delay n/a (mono demo)",
"Confidence n/a"
};
g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 18));
int y = panel.y + 66;
for (String row : rows) {
g.setColor(new Color(43, 53, 68));
g.fillRoundRect(panel.x + 24, y - 24, panel.width - 48, 34, 10, 10);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.drawString(row, panel.x + 42, y);
y += 34;
}
}
private static void drawSpectrogram(Graphics2D g, Rectangle panel) {
drawPanelShell(g, panel, "Spectrogram / waterfall");
int x0 = panel.x + 20;
int y0 = panel.y + 44;
int w = panel.width - 40;
int h = panel.height - 64;
for (int x = 0; x < w; x++) {
float pulse =
SYNTHETIC_SPECTROGRAM_PULSE_BASE
+ SYNTHETIC_SPECTROGRAM_PULSE_SWING
* (float) Math.sin(x / SYNTHETIC_SPECTROGRAM_PULSE_PERIOD);
for (int y = 0; y < h; y++) {
float band =
Math.max(
0f,
1f
- Math.abs(y - h * SYNTHETIC_SPECTROGRAM_BAND_POSITION)
/ SYNTHETIC_SPECTROGRAM_BAND_WIDTH)
* pulse;
g.setColor(
new Color(
16,
Math.min(210, 55 + (int) (band * 155)),
Math.min(255, 90 + (int) (band * 165))));
g.drawLine(x0 + x, y0 + y, x0 + x, y0 + y);
}
}
Rectangle plot = new Rectangle(x0, y0, w, h);
PlotRenderTheme.drawGrid(g, plot, 8, 4);
PlotRenderTheme.drawYAxisLabel(g, plot, "Frequency [Hz]");
PlotRenderTheme.drawYTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"22.1 kHz", "11.0 kHz", "0 Hz"});
PlotRenderTheme.drawXTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"0.0 s", "0.5 s", "1.0 s"});
PlotRenderTheme.drawXAxisLabel(g, plot, "Time [s; older → newer]");
PlotRenderTheme.drawLabel(g, x0 + w - 132, y0 + 14, "Color: relative magnitude [-]");
}
private static void drawDiagnosis(Graphics2D g, Rectangle panel, DiagnosisSnapshot diagnosis) {
drawPanelShell(g, panel, "Diagnosis");
g.setFont(PlotRenderTheme.LABEL_FONT);
int y = panel.y + 58;
if (diagnosis.findings().isEmpty()) {
g.setColor(new Color(150, 210, 180));
g.drawString("INFO Stable single-tone demo; no findings.", panel.x + 24, y);
return;
}
for (DiagnosisFinding finding : diagnosis.findings()) {
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.drawString(
String.format(
Locale.ROOT,
"%s %s (conf %.2f)",
finding.severity(),
finding.message(),
finding.confidence()),
panel.x + 24,
y);
y += 28;
if (y > panel.y + panel.height - 22) {
break;
}
}
}
private static void drawPanelShell(Graphics2D g, Rectangle panel, String title) {
g.setColor(PlotRenderTheme.PANEL_BACKGROUND);
g.fillRoundRect(panel.x, panel.y, panel.width, panel.height, 14, 14);
g.setColor(new Color(72, 84, 102));
g.drawRoundRect(panel.x, panel.y, panel.width, panel.height, 14, 14);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 18));
g.drawString(title, panel.x + 16, panel.y + 28);
}
private static double strongestFrequency(SpectrumSnapshot spectrum) {
int peakBin = 0;
float peak = 0f;
for (int i = 1; i < spectrum.binCount(); i++) {
if (spectrum.magnitude(i) > peak) {
peak = spectrum.magnitude(i);
peakBin = i;
}
}
return spectrum.frequencyOfBin(peakBin);
}
private static BufferedImage renderTrigger() {
SineGenerator gen = new SineGenerator(MONO_44K, 220.0, 0.7f);
AudioBlock block = gen.nextBlock(4096);
WaveformTrigger trigger = new WaveformTrigger(1024);
trigger.setMode(WaveformTrigger.Mode.NORMAL);
trigger.setHoldoffFrames(64);
WaveformTrigger.TriggeredView view = trigger.process(block, 0).orElseThrow();
BufferedImage img = createImage();
Graphics2D g = img.createGraphics();
try {
applyHints(g);
Rectangle plot = new Rectangle(48, 24, W - 64, H - 58);
PlotRenderTheme.drawPlotBackground(g, W, H, plot);
PlotRenderTheme.drawGrid(g, plot, 10, 8);
PlotRenderTheme.drawTitle(g, plot.x, 16, "Waveform (triggered)");
PlotRenderTheme.drawYAxisLabel(g, plot, "Amplitude [-1..1]");
PlotRenderTheme.drawYTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"+1", "0", "-1"});
PlotRenderTheme.drawXAxisLabel(g, plot, "Sample index");
float[] samples = view.samplesView();
int n = samples.length;
int centerY = plot.y + plot.height / 2;
int amplitude = plot.height / 2 - 8;
int[] xs = new int[n];
int[] ys = new int[n];
for (int i = 0; i < n; i++) {
xs[i] = plot.x + (int) ((long) i * (plot.width - 1) / Math.max(1, n - 1));
ys[i] = centerY - (int) (Math.max(-1f, Math.min(1f, samples[i])) * amplitude);
}
g.setColor(PlotRenderTheme.CENTER_LINE);
g.setStroke(PlotRenderTheme.AXIS_STROKE);
g.drawLine(plot.x, centerY, plot.x + plot.width - 1, centerY);
g.setColor(PlotRenderTheme.WAVEFORM_LEFT);
g.setStroke(PlotRenderTheme.TRACE_STROKE);
g.drawPolyline(xs, ys, n);
g.setColor(PlotRenderTheme.TEXT_MUTED);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.drawString(
String.format(
Locale.ROOT,
"Trig: FIRED Slope: rising Level: %+.2f view=1024 samples",
view.level()),
plot.x,
32);
PlotRenderTheme.drawXTicks(
g,
plot,
new double[] {0.0d, 0.5d, 1.0d},
new String[] {"0", Integer.toString(n / 2), Integer.toString(n - 1)});
} finally {
g.dispose();
}
return img;
}
private static BufferedImage renderSpectrumPeakHold() {
SineGenerator gen = new SineGenerator(MONO_44K, 1200.0, 0.6f);
SpectrumAnalyzer analyzer = new SpectrumAnalyzer(FFT, 0, MONO_44K.sampleRate());
SpectrumAverager avg = new SpectrumAverager(0.3f);
PeakHoldSpectrum peak = new PeakHoldSpectrum(0.999f);
for (int i = 0; i < 12; i++) {
AudioBlock b = gen.nextBlock(FFT);
SpectrumSnapshot s = analyzer.analyze(b);
avg.update(s.magnitudesView());
peak.update(s.magnitudesView());
}
SquareGenerator extra = new SquareGenerator(MONO_44K, 4400.0, 0.4f);
AudioBlock burst = extra.nextBlock(FFT);
SpectrumSnapshot burstSnap = analyzer.analyze(burst);
peak.update(burstSnap.magnitudesView());
BufferedImage img = createImage();
Graphics2D g = img.createGraphics();
try {
applyHints(g);
Rectangle plot = new Rectangle(52, 24, W - 68, H - 58);
PlotRenderTheme.drawPlotBackground(g, W, H, plot);
PlotRenderTheme.drawGrid(g, plot, 10, 8);
PlotRenderTheme.drawTitle(g, plot.x, 16, "Spectrum (averaged + peak hold)");
PlotRenderTheme.drawYAxisLabel(g, plot, "Magnitude [dB rel. peak]");
PlotRenderTheme.drawYTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"0 dB", "-40 dB", "-80 dB"});
PlotRenderTheme.drawXTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"0 Hz", "11025 Hz", "22050 Hz"});
PlotRenderTheme.drawXAxisLabel(g, plot, "Frequency [Hz]");
float[] live = avg.averageView();
float[] held = peak.peaks();
int bins = live.length;
float maxMag = 1e-6f;
for (float v : live) {
if (Math.abs(v) > maxMag) {
maxMag = Math.abs(v);
}
}
for (float v : held) {
if (Math.abs(v) > maxMag) {
maxMag = Math.abs(v);
}
}
int floor = plot.y + plot.height - 1;
int top = plot.y + 36;
int[] xs = new int[bins];
int[] ysLive = new int[bins];
int[] ysPeak = new int[bins];
for (int i = 0; i < bins; i++) {
xs[i] = plot.x + (int) ((long) i * (plot.width - 1) / Math.max(1, bins - 1));
ysLive[i] = floor - (int) ((Math.abs(live[i]) / maxMag) * (floor - top));
ysPeak[i] = floor - (int) ((Math.abs(held[i]) / maxMag) * (floor - top));
}
g.setColor(PlotRenderTheme.SPECTRUM_LINE);
g.setStroke(PlotRenderTheme.TRACE_STROKE);
g.drawPolyline(xs, ysLive, bins);
g.setColor(PlotRenderTheme.HIGHLIGHT);
g.setStroke(PlotRenderTheme.PEAK_STROKE);
g.drawPolyline(xs, ysPeak, bins);
g.setColor(PlotRenderTheme.TEXT_MUTED);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.drawString("Legend: blue averaged live spectrum, orange peak hold", plot.x, 32);
} finally {
g.dispose();
}
return img;
}
private static BufferedImage renderRecordingFormat() {
BufferedImage img = createImage();
Graphics2D g = img.createGraphics();
try {
applyHints(g);
g.setColor(PlotRenderTheme.PANEL_BACKGROUND);
g.fillRect(0, 0, W, H);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14));
g.drawString("AudioAnalyzer .aar recording layout (big-endian)", 16, 26);
int y = 56;
int x = 16;
int w = W - 32;
int rowH = 28;
drawBlock(g, x, y, w, rowH, "Header", PlotRenderTheme.SPECTRUM_LINE);
y += rowH;
drawField(g, x + 16, y, "u32 magic 'AAR1'", "u16 version", "u16 channels");
y += rowH;
drawField(g, x + 16, y, "f32 sampleRate", "u16 bitsPerSample", "u16 reserved=0");
y += rowH + 12;
drawBlock(g, x, y, w, rowH, "Frame record (repeats until EOF)", PlotRenderTheme.HIGHLIGHT);
y += rowH;
drawField(g, x + 16, y, "u32 frames", "i64 frameIndex", "i64 timestampNanos");
y += rowH;
drawField(g, x + 16, y, "f32 ch0 sample[0..frames)", "f32 ch1 sample[0..frames)", "...");
y += rowH + 8;
g.setColor(PlotRenderTheme.TEXT_MUTED);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.drawString("Channels are stored non-interleaved within each frame record.", x + 4, y + 16);
g.drawString(
"Reader/writer live in audio-dsp; the format is stable from version 1 onward.",
x + 4,
y + 32);
} finally {
g.dispose();
}
return img;
}
private static void drawBlock(
Graphics2D g, int x, int y, int w, int h, String label, Color color) {
g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 60));
g.fillRect(x, y, w, h);
g.setColor(color);
g.setStroke(new BasicStroke(1.2f));
g.drawRect(x, y, w, h);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.setFont(PlotRenderTheme.TITLE_FONT);
g.drawString(label, x + 8, y + h - 9);
}
private static void drawField(Graphics2D g, int x, int y, String a, String b, String c) {
int colW = (W - 64) / 3;
drawCell(g, x, y, colW, a);
drawCell(g, x + colW + 4, y, colW, b);
drawCell(g, x + 2 * (colW + 4), y, colW, c);
}
private static void drawCell(Graphics2D g, int x, int y, int w, String label) {
g.setColor(PlotRenderTheme.PLOT_BACKGROUND);
g.fillRect(x, y, w, 24);
g.setColor(PlotRenderTheme.AXIS_COLOR);
g.drawRect(x, y, w, 24);
g.setColor(PlotRenderTheme.TEXT_PRIMARY);
g.setFont(PlotRenderTheme.LABEL_FONT);
g.drawString(label, x + 6, y + 16);
}
private static BufferedImage renderAbComparison() {
SpectrumAnalyzer analyzer = new SpectrumAnalyzer(FFT, 0, MONO_44K.sampleRate());
SineGenerator a = new SineGenerator(MONO_44K, 440.0, 0.6f);
SineGenerator b = new SineGenerator(MONO_44K, 880.0, 0.6f);
SpectrumSnapshot sa = null;
SpectrumSnapshot sb = null;
for (int i = 0; i < 6; i++) {
sa = analyzer.analyze(a.nextBlock(FFT));
}
for (int i = 0; i < 6; i++) {
sb = analyzer.analyze(b.nextBlock(FFT));
}
BufferedImage img = createImage();
Graphics2D g = img.createGraphics();
try {
applyHints(g);
g.setColor(PlotRenderTheme.PANEL_BACKGROUND);
g.fillRect(0, 0, W, H);
int halfW = W / 2 - 4;
Rectangle leftPlot = new Rectangle(44, 24, halfW - 56, H - 58);
Rectangle rightPlot = new Rectangle(halfW + 52, 24, halfW - 56, H - 58);
drawSpectrumPanel(g, leftPlot, sa, "A — 440 Hz", PlotRenderTheme.SPECTRUM_LINE);
drawSpectrumPanel(g, rightPlot, sb, "B — 880 Hz", PlotRenderTheme.WAVEFORM_RIGHT);
g.setColor(PlotRenderTheme.HIGHLIGHT);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
g.drawString("|delta dominant freq| ~ 440 Hz", W / 2 - 96, H - 12);
} finally {
g.dispose();
}
return img;
}
private static void drawSpectrumPanel(
Graphics2D g, Rectangle plot, SpectrumSnapshot snap, String title, Color color) {
g.setColor(PlotRenderTheme.PLOT_BACKGROUND);
g.fillRect(plot.x, plot.y, plot.width, plot.height);
PlotRenderTheme.drawGrid(g, plot, 10, 8);
PlotRenderTheme.drawTitle(g, plot.x + 8, plot.y + 16, title);
PlotRenderTheme.drawYAxisLabel(g, plot, "Magnitude [dB rel. peak]");
PlotRenderTheme.drawYTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"0 dB", "-40 dB", "-80 dB"});
PlotRenderTheme.drawXTicks(
g, plot, new double[] {0.0d, 0.5d, 1.0d}, new String[] {"0 Hz", "11025 Hz", "22050 Hz"});
PlotRenderTheme.drawXAxisLabel(g, plot, "Frequency [Hz]");
float[] mag = snap.magnitudesView();
int bins = mag.length;
float max = 1e-6f;
for (float v : mag) {
if (Math.abs(v) > max) {
max = Math.abs(v);
}
}
int floor = plot.y + plot.height - 1;
int top = plot.y + 36;
int[] xs = new int[bins];
int[] ys = new int[bins];
for (int i = 0; i < bins; i++) {
xs[i] = plot.x + (int) ((long) i * (plot.width - 1) / Math.max(1, bins - 1));
ys[i] = floor - (int) ((Math.abs(mag[i]) / max) * (floor - top));
}
g.setColor(color);
g.setStroke(PlotRenderTheme.TRACE_STROKE);
g.drawPolyline(xs, ys, bins);
}
private static BufferedImage createImage() {
return new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
}
private static void applyHints(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
private static void writePng(Path out, BufferedImage img) throws IOException {
if (!ImageIO.write(img, "png", out.toFile())) {
throw new IOException("PNG writer not available for " + out);
}
}
}