WaveformRenderer.java
package org.hammer.audio.ui;
import org.hammer.audio.snapshot.WaveformSnapshot;
/**
* Pure-function pixel scaling for waveform rendering.
*
* <p>This is the only place in the platform that knows about panel dimensions and pixel
* coordinates. Audio capture, ring buffering, DSP and analysis all stay in normalized {@code float}
* space; renderers in this package convert immutable {@link WaveformSnapshot}s into the {@code (x,
* y)} integer arrays a Swing or JavaFX canvas can {@code drawPolyline}.
*
* <p>Renderers are stateless: the same instance can be invoked from any thread and reused across
* snapshots. They allocate a fresh result on every call to keep the data immutable from the
* caller's perspective; for high-rate rendering paths a future variant can accept caller-owned
* scratch arrays.
*
* @author refactoring
*/
public final class WaveformRenderer {
private WaveformRenderer() {
// Utility class
}
/**
* Compute X-coordinates that span {@code [0, panelWidth - 1]} for {@code points} samples.
*
* @param points number of samples on the X axis; must be {@code >= 0}
* @param panelWidth target panel width in pixels
* @return integer array of length {@code points}; values are monotonically non-decreasing
*/
public static int[] computeXPoints(int points, int panelWidth) {
int[] xs = new int[Math.max(0, points)];
if (xs.length == 0) {
return xs;
}
if (xs.length == 1) {
xs[0] = 0;
return xs;
}
final int panelW = panelWidth - 1;
final int pointsM1 = xs.length - 1;
for (int i = 0; i < xs.length; i++) {
xs[i] = (int) ((long) panelW * i / pointsM1);
}
return xs;
}
/**
* Convert one channel of a {@link WaveformSnapshot} into pixel-space Y-coordinates.
*
* <p>Samples in the snapshot are normalized to {@code [-1, 1]} (the audio domain). This method
* maps {@code +1.0f → 0} (top) and {@code -1.0f → panelHeight} (bottom), so the centre of the
* panel corresponds to silence.
*
* <p>If the snapshot legitimately has zero frames an empty array is returned. Contract violations
* on the inputs throw eagerly so that misconfigured rendering code surfaces as a clear failure
* rather than silently drawing nothing.
*
* @param snapshot waveform snapshot; never {@code null}
* @param channel channel index, in {@code [0, snapshot.channels())}
* @param panelHeight target panel height in pixels; must be {@code > 0}
* @return integer Y-coordinates of length {@code snapshot.frames()}
* @throws IllegalArgumentException if {@code panelHeight <= 0}
* @throws IndexOutOfBoundsException if {@code channel} is outside {@code [0,
* snapshot.channels())}
*/
public static int[] computeYPoints(WaveformSnapshot snapshot, int channel, int panelHeight) {
if (panelHeight <= 0) {
throw new IllegalArgumentException("panelHeight must be > 0, was " + panelHeight);
}
if (channel < 0 || channel >= snapshot.channels()) {
throw new IndexOutOfBoundsException(
"channel " + channel + " out of range [0, " + snapshot.channels() + ")");
}
float[] samples = snapshot.channelView(channel);
int[] ys = new int[samples.length];
final float halfH = panelHeight / 2f;
for (int i = 0; i < samples.length; i++) {
float n = samples[i];
if (n > 1f) {
n = 1f;
} else if (n < -1f) {
n = -1f;
}
ys[i] = Math.round(halfH - n * halfH);
}
return ys;
}
/**
* Convert all channels at once.
*
* <p>Returns an empty outer array when the snapshot has zero channels (e.g. {@link
* WaveformSnapshot#EMPTY}); otherwise delegates to {@link #computeYPoints} per channel and
* propagates any contract violation it raises.
*
* @param snapshot waveform snapshot
* @param panelHeight target panel height in pixels; must be {@code > 0}
* @return integer Y-coordinate arrays, one per channel
* @throws IllegalArgumentException if {@code panelHeight <= 0} and the snapshot has at least one
* channel
*/
public static int[][] computeYPointsAllChannels(WaveformSnapshot snapshot, int panelHeight) {
int[][] result = new int[snapshot.channels()][];
for (int c = 0; c < snapshot.channels(); c++) {
result[c] = computeYPoints(snapshot, c, panelHeight);
}
return result;
}
}