SampleDecoder.java

package org.hammer.audio.capture;

import org.hammer.audio.core.AudioFormatDescriptor;

/**
 * Stateless decoder of interleaved PCM byte buffers into normalized {@code float[channels][frames]}
 * sample arrays.
 *
 * <p>Supports the formats used by the platform's capture path:
 *
 * <ul>
 *   <li>signed/unsigned 8-bit PCM
 *   <li>signed/unsigned 16-bit PCM, little- or big-endian
 *   <li>generic 1..4-byte big-endian fallback for any other sample size
 * </ul>
 *
 * <p>Output samples are normalized to {@code [-1.0f, 1.0f]} for signed formats and to {@code [-1,
 * 1]} for unsigned formats (mapped from {@code [0, max]} to {@code [-1, 1]}).
 *
 * <p>This class is stateless and thread-safe.
 *
 * @author refactoring
 */
public final class SampleDecoder {

  private final AudioFormatDescriptor descriptor;
  private final int bytesPerSample;
  private final int frameSize;
  private final boolean signed;
  private final boolean bigEndian;
  private final float signedScale;
  private final float unsignedScale;
  private final int unsignedOffset;

  /**
   * @param descriptor audio format descriptor
   * @param signed true if source samples are signed
   * @param bigEndian true if source samples are big-endian (16-bit and wider)
   */
  public SampleDecoder(AudioFormatDescriptor descriptor, boolean signed, boolean bigEndian) {
    this.descriptor = descriptor;
    this.signed = signed;
    this.bigEndian = bigEndian;
    this.bytesPerSample = Math.max(1, descriptor.sourceSampleSizeInBits() / 8);
    this.frameSize = bytesPerSample * descriptor.channels();
    int bits = descriptor.sourceSampleSizeInBits();
    this.signedScale = 1f / ((1 << (bits - 1)) - 1);
    int unsignedMax = (1 << bits) - 1;
    this.unsignedScale = 2f / unsignedMax;
    this.unsignedOffset = unsignedMax / 2;
  }

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

  /**
   * @return number of bytes per single-channel sample
   */
  public int bytesPerSample() {
    return bytesPerSample;
  }

  /**
   * @return number of bytes per frame ({@code bytesPerSample * channels})
   */
  public int frameSize() {
    return frameSize;
  }

  /**
   * @param byteCount number of bytes
   * @return number of complete frames in that byte count
   */
  public int framesIn(int byteCount) {
    return byteCount / frameSize;
  }

  /**
   * Decode raw bytes into a pre-allocated {@code float[channels][frames]} buffer. Existing samples
   * beyond the decoded range are not touched.
   *
   * @param data raw interleaved PCM bytes
   * @param byteCount number of valid bytes in {@code data}
   * @param dest destination buffer of shape {@code [channels][>=framesIn(byteCount)]}
   * @return number of decoded frames
   */
  public int decode(byte[] data, int byteCount, float[][] dest) {
    int frames = framesIn(byteCount);
    int channels = descriptor.channels();
    for (int frame = 0; frame < frames; frame++) {
      int frameOffset = frame * frameSize;
      for (int ch = 0; ch < channels; ch++) {
        int sampleOffset = frameOffset + ch * bytesPerSample;
        dest[ch][frame] = decodeOne(data, sampleOffset);
      }
    }
    return frames;
  }

  private float decodeOne(byte[] data, int offset) {
    if (bytesPerSample == 1) {
      int b = data[offset] & 0xFF;
      if (signed) {
        return (byte) b * signedScale;
      }
      return (b - unsignedOffset) * unsignedScale;
    }
    if (bytesPerSample == 2) {
      int hi = data[offset + (bigEndian ? 0 : 1)] & 0xFF;
      int lo = data[offset + (bigEndian ? 1 : 0)] & 0xFF;
      int raw = (hi << 8) | lo;
      if (signed) {
        return ((short) raw) * signedScale;
      }
      return ((raw & 0xFFFF) - unsignedOffset) * unsignedScale;
    }
    // Generic big-endian fallback for non-standard sizes.
    int sample = 0;
    for (int b = 0; b < bytesPerSample; b++) {
      sample = (sample << 8) | (data[offset + b] & 0xFF);
    }
    int bits = descriptor.sourceSampleSizeInBits();
    if (signed) {
      int shift = 32 - bits;
      sample = (sample << shift) >> shift;
      return sample * signedScale;
    }
    return (sample - unsignedOffset) * unsignedScale;
  }
}