Class WavSampleReader

java.lang.Object
com.tino1b2be.dtmf.io.wav.internal.WavSampleReader

public final class WavSampleReader extends Object
Frame-by-frame decoder for the data payload of a validated WAV stream. Given a WaveFormat describing the stream and a byte source (either a FileChannel or an InputStream) positioned at the first byte of the payload, this reader produces normalised double frames on demand.

The reader is the "inner engine" of com.tino1b2be.dtmf.io.wav.WavAudioSource — the public AudioSource methods delegate straight through here. The separation matters for two reasons:

  1. Decode arithmetic is tuple-driven (bitDepth × encoding × endianness) and the actual byte-»-double formulas live exactly once in SampleConversion. Keeping that dispatch here, rather than duplicating it in WavAudioSource, means the RawPcmAudioSource / WAV / MP3 providers all walk through the same normalised code path.
  2. The two byte-source modes (FileChannel vs. InputStream) differ only in how raw bytes are pulled — never in how samples are decoded — so the polymorphism lives at the reader layer and WavAudioSource does not have to branch on source type per read.

Endianness

WAV is always little-endian (the RIFF specification fixes this). The reader hard-wires ByteOrder.LITTLE_ENDIAN when asking SampleConversion.decoderFor(int, ByteOrder, PcmEncoding) for a decoder; callers do not specify byte order.

Encoding mapping

The WaveFormat.Encoding enum carried by WaveFormat is mapped to the shared PcmEncoding as follows: WaveFormat's compact constructor has already validated every tuple the parser produces, so SampleConversion.decoderFor(int, ByteOrder, PcmEncoding) is called with arguments that cannot possibly fall into its IllegalArgumentException branch — but the dispatch keeps the fallback intact anyway, so a bug in the parser upstream fails loudly rather than silently producing garbled samples.

Read loop

Each call to readFrames(buffer, offset, framesToRead):
  1. Computes how many frames remain in the payload (totalFrames - frameCursor).
  2. Returns -1 if no frames remain and none were requested (i.e. already at EOS).
  3. Otherwise caps framesToRead at the remaining count, pulls framesActuallyRead * bytesPerFrame raw bytes from the byte source into a scratch buffer, and walks the scratch buffer one sample at a time, writing buffer[offset + frameIndex * channelCount + channelIndex] using the pre-selected SampleConversion.SampleDecoder.
  4. Advances frameCursor() and returns framesActuallyRead.

Zero is a valid return value when the caller asks for zero frames; the -1 sentinel only appears when the caller asks for a positive frame count after the payload has been fully consumed. This matches the read() contract of InputStream and of AudioSource itself (Requirement 3.6).

Seek

The reader itself does not seek — it tracks frameCursor strictly forward as readFrames(double[], int, int) consumes bytes. Random-access seeking is a property of the enclosing WavAudioSource, which (when backed by a FileChannel) moves the channel's position and then calls seekToFrame(long) to keep this reader's internal counter in sync. InputStream-backed sources do not expose seek to callers (Requirement 3.9, 3.11), so the reader's counter on the stream path only ever moves forward through readFrames.

Closing

The reader does not own the byte source. Closing the underlying FileChannel or InputStream is the caller's responsibility (and the caller's responsibility alone — per Requirement 4.10, caller-supplied streams are never closed by the provider). This class deliberately offers no close method.

Not thread-safe. A single reader mediates mutable byte-source state and a mutable frame cursor; concurrent readFrames(double[], int, int) calls are undefined behaviour.

This class is not part of the published API. It lives in com.tino1b2be.dtmf.io.wav.internal, whose stability contract (see the package Javadoc) explicitly allows breakage between any two releases. It is public at the type level purely so classes in the parent com.tino1b2be.dtmf.io.wav package can reach it; external callers MUST NOT depend on it.

Since:
2.1.0
  • Constructor Details

    • WavSampleReader

      public WavSampleReader(WaveFormat format, FileChannel channel)
      Build a reader backed by a random-access FileChannel.

      The channel MUST be positioned at the first byte of the data chunk's payload. The RIFF parser in this package leaves the channel at that position after reading the 8-byte "data" | size header, and the resulting readFrames(double[], int, int) calls advance the channel position in lockstep.

      The reader does not take ownership of the channel: closing the reader does not close the channel, and there is no close method. The caller (the WAV provider's open(Path) branch via WavAudioSource) owns the channel.

      Parameters:
      format - validated WAV metadata; must be non-null
      channel - open file channel positioned at the start of the data payload; must be non-null
      Throws:
      NullPointerException - if either argument is null
    • WavSampleReader

      public WavSampleReader(WaveFormat format, InputStream stream)
      Build a reader backed by a forward-only InputStream.

      The stream MUST be positioned at the first byte of the data chunk's payload. The RIFF parser in this package leaves the stream at that position after reading the 8-byte "data" | size header; subsequent readFrames(double[], int, int) calls consume bytes from the stream strictly in order.

      The reader does not take ownership of the stream: closing the reader does not close the stream, and there is no close method. Caller-supplied streams are never closed by the provider (Requirement 4.10); the caller is responsible for closing whatever it passed in.

      Parameters:
      format - validated WAV metadata; must be non-null
      stream - open input stream positioned at the start of the data payload; must be non-null
      Throws:
      NullPointerException - if either argument is null
  • Method Details

    • readFrames

      public int readFrames(double[] buffer, int offset, int framesToRead) throws IOException
      Read up to framesToRead frames from the payload into buffer starting at offset.

      Frames are interleaved in the buffer as [offset + i * channelCount + c] for frame i and channel c (left at c = 0, right at c = 1 for stereo; mono has a single channel per frame). Samples are normalised to [-1.0, 1.0] via SampleConversion.SampleDecoder, matching the AudioSource.read(...) contract (Requirements 3.6, 9.14).

      Parameters:
      buffer - destination buffer; must be non-null and large enough to hold framesToRead * channelCount samples starting at offset
      offset - zero-based index of the first sample slot to write; must be non-negative
      framesToRead - maximum number of frames to read; must be non-negative
      Returns:
      the number of frames actually read (in [0, framesToRead]), or -1 when the reader has reached end-of-stream (i.e. frameCursor has already advanced to totalFrames) and the caller asked for a positive frame count
      Throws:
      NullPointerException - if buffer is null
      IllegalArgumentException - if offset or framesToRead is negative, or if offset + framesToRead * channelCount exceeds buffer.length
      IOException - if the underlying byte source throws during the pull, including EOFException when the payload is shorter than the declared dataSizeBytes
    • seekToFrame

      public void seekToFrame(long frameIndex)
      Reset the reader's internal frame cursor to frameIndex.

      This method is a bookkeeping operation only: it does not move the byte source. The enclosing WavAudioSource is responsible for repositioning its FileChannel (the only byte-source mode that supports seeking) to dataStartByteOffset + frameIndex * bytesPerFrame before calling this method. Splitting the two operations keeps this reader free of knowledge about the absolute byte offsets of the enclosing RIFF container.

      Parameters:
      frameIndex - new frame cursor value; must be in [0, totalFrames()]
      Throws:
      IllegalArgumentException - if frameIndex is out of range
    • format

      public WaveFormat format()
      Returns:
      the parsed WAV metadata this reader was built with
    • frameCursor

      public long frameCursor()
      Returns:
      the zero-based index of the next frame this reader will decode. Starts at zero, increases strictly monotonically through readFrames(double[], int, int) calls, and is reset only via seekToFrame(long).
    • totalFrames

      public long totalFrames()
      Returns:
      the total number of frames declared by the WAV header (cached from WaveFormat.totalFrames()). Reaching this value makes subsequent readFrames(double[], int, int) calls return -1.