WaveformSnapshot.java
package org.hammer.audio.snapshot;
import org.hammer.audio.core.AudioBlock;
/**
* Immutable, UI-friendly snapshot of one block of waveform data.
*
* <p>Unlike the legacy {@link org.hammer.audio.WaveformModel}, which carried pre-computed pixel
* coordinates and thus mixed audio and rendering concerns, a {@code WaveformSnapshot} holds only
* normalized {@code float} samples plus the source metadata. Pixel scaling for any specific canvas
* is performed by the rendering layer (e.g. {@link org.hammer.audio.ui.WaveformRenderer}).
*
* <p>Snapshots are cheap to consume by any UI toolkit (Swing, JavaFX, Web), and are
* serializable-friendly for export and remote APIs.
*
* @author refactoring
*/
public final class WaveformSnapshot {
private final float[][] samples; // [channel][frame]
private final int frames;
private final float sampleRate;
private final long sourceFrameIndex;
private final long sourceTimestampNanos;
/**
* Build a waveform snapshot from a freshly-allocated {@code float[channels][frames]} array. The
* caller transfers ownership of the array and must not mutate it after the call.
*
* @param samples per-channel sample arrays of equal length (ownership transferred)
* @param sampleRate source sample rate in Hz
* @param sourceFrameIndex frame index of the source block
* @param sourceTimestampNanos timestamp of the source block (nanos)
* @return a new immutable snapshot
*/
public static WaveformSnapshot wrap(
float[][] samples, float sampleRate, long sourceFrameIndex, long sourceTimestampNanos) {
return new WaveformSnapshot(samples, sampleRate, sourceFrameIndex, sourceTimestampNanos, false);
}
/**
* Convenience factory that builds a snapshot from the contents of an {@link AudioBlock}.
*
* <p>Exactly one defensive deep copy of the per-channel sample arrays is performed: it comes from
* {@link AudioBlock#samples()}. The {@code copy=false} flag passed to the private constructor
* below means we do <em>not</em> copy a second time. Do not flip that flag to {@code true}
* without removing the {@code block.samples()} call, or every UI snapshot will allocate twice.
*/
public static WaveformSnapshot fromBlock(AudioBlock block) {
// block.samples() already returns a fresh deep copy; ownership transfers to the snapshot.
return new WaveformSnapshot(
block.samples(),
block.format().sampleRate(),
block.frameIndex(),
block.timestampNanos(),
false);
}
/** Empty snapshot constant. */
public static final WaveformSnapshot EMPTY =
new WaveformSnapshot(new float[0][], 0f, 0L, 0L, false);
private WaveformSnapshot(
float[][] samples,
float sampleRate,
long sourceFrameIndex,
long sourceTimestampNanos,
boolean copy) {
if (copy) {
float[][] cp = new float[samples.length][];
for (int c = 0; c < samples.length; c++) {
cp[c] = samples[c].clone();
}
this.samples = cp;
} else {
this.samples = samples;
}
this.frames = samples.length == 0 || samples[0] == null ? 0 : samples[0].length;
this.sampleRate = sampleRate;
this.sourceFrameIndex = sourceFrameIndex;
this.sourceTimestampNanos = sourceTimestampNanos;
}
/**
* @return number of channels
*/
public int channels() {
return samples.length;
}
/**
* @return number of frames per channel
*/
public int frames() {
return frames;
}
/**
* @return source sample rate in Hz
*/
public float sampleRate() {
return sampleRate;
}
/**
* @return source block frame index
*/
public long sourceFrameIndex() {
return sourceFrameIndex;
}
/**
* @return source block timestamp in nanos
*/
public long sourceTimestampNanos() {
return sourceTimestampNanos;
}
/**
* Read-only view of one channel. The returned array is the snapshot's internal storage and
* <strong>must not be mutated</strong>.
*
* @param channel channel index
* @return internal sample array (do not mutate)
*/
public float[] channelView(int channel) {
return samples[channel];
}
}