AudioBlock.java

package org.hammer.audio.core;

import java.util.Objects;

/**
 * Immutable block of normalized audio frames flowing through the platform.
 *
 * <p>An {@code AudioBlock} is the canonical unit of audio data exchanged between the capture, ring
 * buffer, DSP, analysis and snapshot layers. It carries:
 *
 * <ul>
 *   <li>{@link #format()} — descriptor of the underlying stream
 *   <li>{@link #samples()} — non-interleaved {@code float[channels][frames]} samples in the
 *       normalized range {@code [-1.0f, 1.0f]}
 *   <li>{@link #frameIndex()} — running frame counter from stream start (monotonically increasing)
 *   <li>{@link #timestampNanos()} — capture timestamp in nanoseconds (relative to {@link
 *       System#nanoTime()})
 * </ul>
 *
 * <p>Blocks are immutable: the underlying {@code float[][]} is defensively copied on construction
 * and on access. This makes them safe to hand out to multiple consumers without coordination, at
 * the cost of an extra allocation per snapshot. Hot DSP paths that consume blocks should use {@link
 * #channelView(int)} which exposes the internal array read-only by contract (the array is still
 * shared, do not mutate).
 *
 * <p>Thread-safety: instances are immutable and safely publishable.
 *
 * @author refactoring
 */
public final class AudioBlock {

  private final AudioFormatDescriptor format;
  private final float[][] samples; // [channel][frame]
  private final int frames;
  private final long frameIndex;
  private final long timestampNanos;

  /**
   * Create an audio block by defensively copying the supplied samples.
   *
   * @param format audio format descriptor; must not be {@code null}
   * @param samples non-interleaved {@code float[channels][frames]} buffer; must match {@code
   *     format.channels()} and have a uniform per-channel length. Must not be {@code null}
   * @param frameIndex monotonically increasing frame counter from stream start
   * @param timestampNanos capture timestamp (nanoseconds, e.g. {@link System#nanoTime()})
   * @throws IllegalArgumentException if {@code samples} layout does not match {@code format}
   */
  public AudioBlock(
      AudioFormatDescriptor format, float[][] samples, long frameIndex, long timestampNanos) {
    this(format, samples, frameIndex, timestampNanos, true);
  }

  /**
   * Internal constructor that may skip the defensive copy. Used by {@link #wrap} for hot-path
   * production code that has just allocated a fresh array and is willing to surrender ownership.
   */
  private AudioBlock(
      AudioFormatDescriptor format,
      float[][] samples,
      long frameIndex,
      long timestampNanos,
      boolean copy) {
    this.format = Objects.requireNonNull(format, "format");
    Objects.requireNonNull(samples, "samples");
    if (samples.length != format.channels()) {
      throw new IllegalArgumentException(
          "samples.length (" + samples.length + ") != format.channels (" + format.channels() + ")");
    }
    int len = samples.length == 0 ? 0 : (samples[0] == null ? 0 : samples[0].length);
    for (int c = 0; c < samples.length; c++) {
      if (samples[c] == null) {
        throw new IllegalArgumentException("samples[" + c + "] is null");
      }
      if (samples[c].length != len) {
        throw new IllegalArgumentException(
            "All channels must have the same number of frames; channel "
                + c
                + " has "
                + samples[c].length
                + " expected "
                + len);
      }
    }
    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 = len;
    this.frameIndex = frameIndex;
    this.timestampNanos = timestampNanos;
  }

  /**
   * Wrap an already-owned {@code float[channels][frames]} array as an {@code AudioBlock}, skipping
   * the per-channel deep copy. The caller transfers ownership of {@code samples} and must not
   * mutate it after the call.
   *
   * <p>Layout validation (channel count, uniform per-channel length) is still performed; only the
   * deep copy is skipped. This is the recommended factory for hot paths that have just allocated a
   * fresh, exactly-sized array (e.g. capture loops, signal generators).
   *
   * @param format audio format descriptor; must not be {@code null}
   * @param samples non-interleaved per-channel buffer (ownership transferred); must not be {@code
   *     null}
   * @param frameIndex monotonically increasing frame counter from stream start
   * @param timestampNanos capture timestamp (nanoseconds)
   * @return a new immutable {@code AudioBlock} referencing the supplied array
   */
  public static AudioBlock wrap(
      AudioFormatDescriptor format, float[][] samples, long frameIndex, long timestampNanos) {
    return new AudioBlock(format, samples, frameIndex, timestampNanos, false);
  }

  /**
   * @return the audio format descriptor
   */
  public AudioFormatDescriptor format() {
    return format;
  }

  /**
   * @return a defensive deep copy of the per-channel sample arrays. For zero-allocation read access
   *     in hot paths use {@link #channelView(int)}.
   */
  public float[][] samples() {
    float[][] cp = new float[samples.length][];
    for (int c = 0; c < samples.length; c++) {
      cp[c] = samples[c].clone();
    }
    return cp;
  }

  /**
   * Read-only view of one channel's sample array. The returned array is the block's internal
   * storage and <strong>must not be mutated</strong> by the caller. This exists for hot DSP loops
   * that must avoid per-block allocations.
   *
   * @param channel channel index, in {@code [0, channels)}
   * @return the internal per-channel sample array (do not mutate)
   * @throws IndexOutOfBoundsException if {@code channel} is out of range
   */
  public float[] channelView(int channel) {
    return samples[channel];
  }

  /**
   * @return number of audio frames in this block (samples per channel)
   */
  public int frames() {
    return frames;
  }

  /**
   * @return number of channels (convenience alias of {@code format().channels()})
   */
  public int channels() {
    return format.channels();
  }

  /**
   * @return monotonically increasing frame counter from stream start
   */
  public long frameIndex() {
    return frameIndex;
  }

  /**
   * @return capture timestamp in nanoseconds (cf. {@link System#nanoTime()})
   */
  public long timestampNanos() {
    return timestampNanos;
  }

  @Override
  public String toString() {
    return "AudioBlock[" + format + ", frames=" + frames + ", frameIndex=" + frameIndex + "]";
  }
}