WaveformModel.java

package org.hammer.audio;

import java.util.Arrays;

/**
 * Immutable snapshot of waveform data for rendering.
 *
 * <p>This class represents a point-in-time snapshot of audio waveform data, containing x and y
 * coordinates for drawing, as well as metadata about tick intervals.
 *
 * <p>Thread-safety: This class is immutable and thread-safe. All arrays are defensive copies,
 * preventing external modification.
 *
 * @author refactoring
 */
public final class WaveformModel {

  /** Empty WaveformModel instance for use when no data is available. */
  public static final WaveformModel EMPTY = new WaveformModel(new int[0], new int[0][], 0, 0);

  private final int[] xPoints;
  private final int[][] yPoints;
  private final int tickEveryNSample;
  private final int numberOfPoints;
  private final int dataSize;

  /**
   * Create a new WaveformModel.
   *
   * @param xPoints x-coordinates for drawing (will be copied)
   * @param yPoints y-coordinates for each channel (will be deep copied)
   * @param tickEveryNSample interval between tick marks
   * @param dataSize the buffer data size in bytes
   */
  public WaveformModel(int[] xPoints, int[][] yPoints, int tickEveryNSample, int dataSize) {
    // Defensive copies
    this.xPoints = xPoints != null ? Arrays.copyOf(xPoints, xPoints.length) : new int[0];

    if (yPoints != null) {
      this.yPoints = new int[yPoints.length][];
      for (int i = 0; i < yPoints.length; i++) {
        this.yPoints[i] =
            yPoints[i] != null ? Arrays.copyOf(yPoints[i], yPoints[i].length) : new int[0];
      }
    } else {
      this.yPoints = new int[0][];
    }

    this.tickEveryNSample = tickEveryNSample;
    this.numberOfPoints = xPoints != null ? xPoints.length : 0;
    this.dataSize = dataSize;
  }

  /**
   * Get x-coordinates for drawing.
   *
   * @return defensive copy of x-coordinates array
   */
  public int[] getXPoints() {
    return Arrays.copyOf(xPoints, xPoints.length);
  }

  /**
   * Get y-coordinates for all channels.
   *
   * @return defensive deep copy of y-coordinates array
   */
  public int[][] getYPoints() {
    int[][] copy = new int[yPoints.length][];
    for (int i = 0; i < yPoints.length; i++) {
      copy[i] = Arrays.copyOf(yPoints[i], yPoints[i].length);
    }
    return copy;
  }

  /**
   * Get y-coordinates for a specific channel.
   *
   * @param channel the channel index
   * @return defensive copy of y-coordinates for the channel, or empty array if invalid
   */
  public int[] getYPointsForChannel(int channel) {
    if (channel >= 0 && channel < yPoints.length) {
      return Arrays.copyOf(yPoints[channel], yPoints[channel].length);
    }
    return new int[0];
  }

  /**
   * Get the tick interval in samples.
   *
   * @return samples between tick marks
   */
  public int getTickEveryNSample() {
    return tickEveryNSample;
  }

  /**
   * Get the number of points in the waveform.
   *
   * @return number of points
   */
  public int getNumberOfPoints() {
    return numberOfPoints;
  }

  /**
   * Get the number of audio channels.
   *
   * @return number of channels
   */
  public int getChannelCount() {
    return yPoints.length;
  }

  /**
   * Get the data buffer size in bytes.
   *
   * @return data size
   */
  public int getDataSize() {
    return dataSize;
  }

  @Override
  public String toString() {
    return String.format(
        "WaveformModel[points=%d, channels=%d, tick=%d, dataSize=%d]",
        numberOfPoints, yPoints.length, tickEveryNSample, dataSize);
  }
}