AudioFormatDescriptor.java

package org.hammer.audio.core;

import java.util.Objects;

/**
 * Immutable, audio-domain-only description of an audio stream.
 *
 * <p>This is the platform's canonical format descriptor. Unlike {@link
 * javax.sound.sampled.AudioFormat}, it is:
 *
 * <ul>
 *   <li>UI/JavaSound-independent (no pixels, no Swing, no platform types)
 *   <li>immutable and thread-safe (all fields {@code final})
 *   <li>cheap to share between the capture, DSP, analysis and UI layers
 * </ul>
 *
 * <p>It always describes a stream of <em>normalized floating-point</em> samples. The original
 * device sample size in bits is retained for diagnostics and back-conversion only; downstream DSP
 * and analysis modules must operate on the normalized {@code float} representation.
 *
 * @author refactoring
 */
public final class AudioFormatDescriptor {

  private final float sampleRate;
  private final int channels;
  private final int sourceSampleSizeInBits;

  /**
   * Create a new immutable audio format descriptor.
   *
   * @param sampleRate sample rate in Hz; must be {@code > 0}
   * @param channels number of channels (e.g. 1 for mono, 2 for stereo); must be {@code >= 1}
   * @param sourceSampleSizeInBits source device sample size in bits (e.g. 8 or 16); must be {@code
   *     >= 1}. Retained for diagnostics, not used in DSP math.
   * @throws IllegalArgumentException if any argument is invalid
   */
  public AudioFormatDescriptor(float sampleRate, int channels, int sourceSampleSizeInBits) {
    if (!(sampleRate > 0f) || Float.isNaN(sampleRate) || Float.isInfinite(sampleRate)) {
      throw new IllegalArgumentException("sampleRate must be > 0, was " + sampleRate);
    }
    if (channels < 1) {
      throw new IllegalArgumentException("channels must be >= 1, was " + channels);
    }
    if (sourceSampleSizeInBits < 1) {
      throw new IllegalArgumentException(
          "sourceSampleSizeInBits must be >= 1, was " + sourceSampleSizeInBits);
    }
    this.sampleRate = sampleRate;
    this.channels = channels;
    this.sourceSampleSizeInBits = sourceSampleSizeInBits;
  }

  /**
   * @return the sample rate in Hz (e.g. {@code 44100.0f})
   */
  public float sampleRate() {
    return sampleRate;
  }

  /**
   * @return the number of channels (e.g. 1 for mono, 2 for stereo)
   */
  public int channels() {
    return channels;
  }

  /**
   * @return the source device sample size in bits (typically 8, 16 or 24). Provided for diagnostics
   *     only; DSP code should not depend on this.
   */
  public int sourceSampleSizeInBits() {
    return sourceSampleSizeInBits;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof AudioFormatDescriptor other)) {
      return false;
    }
    return Float.compare(other.sampleRate, sampleRate) == 0
        && other.channels == channels
        && other.sourceSampleSizeInBits == sourceSampleSizeInBits;
  }

  @Override
  public int hashCode() {
    return Objects.hash(sampleRate, channels, sourceSampleSizeInBits);
  }

  @Override
  public String toString() {
    return "AudioFormatDescriptor[sampleRate="
        + sampleRate
        + "Hz, channels="
        + channels
        + ", sourceBits="
        + sourceSampleSizeInBits
        + "]";
  }
}