Class WavSampleReader
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:
- Decode arithmetic is tuple-driven (
bitDepth×encoding× endianness) and the actual byte-»-doubleformulas live exactly once inSampleConversion. Keeping that dispatch here, rather than duplicating it inWavAudioSource, means theRawPcmAudioSource/ WAV / MP3 providers all walk through the same normalised code path. - The two byte-source modes (
FileChannelvs.InputStream) differ only in how raw bytes are pulled — never in how samples are decoded — so the polymorphism lives at the reader layer andWavAudioSourcedoes not have to branch on source type per read.
Endianness
WAV is always little-endian (the RIFF specification fixes this). The reader hard-wiresByteOrder.LITTLE_ENDIAN when asking
SampleConversion.decoderFor(int, ByteOrder, PcmEncoding) for a
decoder; callers do not specify byte order.
Encoding mapping
TheWaveFormat.Encoding enum carried by WaveFormat is
mapped to the shared PcmEncoding as follows:
WaveFormat.Encoding.PCM_SIGNED→PcmEncoding.SIGNED_INT, valid at bit depths{16, 24, 32}.WaveFormat.Encoding.IEEE_FLOAT→PcmEncoding.IEEE_FLOAT, valid at bit depths{32, 64}.
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 toreadFrames(buffer, offset, framesToRead):
- Computes how many frames remain in the payload
(
totalFrames - frameCursor). - Returns
-1if no frames remain and none were requested (i.e. already at EOS). - Otherwise caps
framesToReadat the remaining count, pullsframesActuallyRead * bytesPerFrameraw bytes from the byte source into a scratch buffer, and walks the scratch buffer one sample at a time, writingbuffer[offset + frameIndex * channelCount + channelIndex]using the pre-selectedSampleConversion.SampleDecoder. - Advances
frameCursor()and returnsframesActuallyRead.
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 tracksframeCursor
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 underlyingFileChannel 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 Summary
ConstructorsConstructorDescriptionWavSampleReader(WaveFormat format, InputStream stream) Build a reader backed by a forward-onlyInputStream.WavSampleReader(WaveFormat format, FileChannel channel) Build a reader backed by a random-accessFileChannel. -
Method Summary
Modifier and TypeMethodDescriptionformat()longintreadFrames(double[] buffer, int offset, int framesToRead) Read up toframesToReadframes from the payload intobufferstarting atoffset.voidseekToFrame(long frameIndex) Reset the reader's internal frame cursor toframeIndex.long
-
Constructor Details
-
WavSampleReader
Build a reader backed by a random-accessFileChannel.The channel MUST be positioned at the first byte of the
datachunk's payload. The RIFF parser in this package leaves the channel at that position after reading the 8-byte"data" | sizeheader, and the resultingreadFrames(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 viaWavAudioSource) owns the channel.- Parameters:
format- validated WAV metadata; must be non-nullchannel- open file channel positioned at the start of thedatapayload; must be non-null- Throws:
NullPointerException- if either argument isnull
-
WavSampleReader
Build a reader backed by a forward-onlyInputStream.The stream MUST be positioned at the first byte of the
datachunk's payload. The RIFF parser in this package leaves the stream at that position after reading the 8-byte"data" | sizeheader; subsequentreadFrames(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-nullstream- open input stream positioned at the start of thedatapayload; must be non-null- Throws:
NullPointerException- if either argument isnull
-
-
Method Details
-
readFrames
Read up toframesToReadframes from the payload intobufferstarting atoffset.Frames are interleaved in the buffer as
[offset + i * channelCount + c]for frameiand channelc(left atc = 0, right atc = 1for stereo; mono has a single channel per frame). Samples are normalised to[-1.0, 1.0]viaSampleConversion.SampleDecoder, matching theAudioSource.read(...)contract (Requirements 3.6, 9.14).- Parameters:
buffer- destination buffer; must be non-null and large enough to holdframesToRead * channelCountsamples starting atoffsetoffset- zero-based index of the first sample slot to write; must be non-negativeframesToRead- maximum number of frames to read; must be non-negative- Returns:
- the number of frames actually read (in
[0, framesToRead]), or-1when the reader has reached end-of-stream (i.e.frameCursorhas already advanced tototalFrames) and the caller asked for a positive frame count - Throws:
NullPointerException- ifbufferisnullIllegalArgumentException- ifoffsetorframesToReadis negative, or ifoffset + framesToRead * channelCountexceedsbuffer.lengthIOException- if the underlying byte source throws during the pull, includingEOFExceptionwhen the payload is shorter than the declareddataSizeBytes
-
seekToFrame
public void seekToFrame(long frameIndex) Reset the reader's internal frame cursor toframeIndex.This method is a bookkeeping operation only: it does not move the byte source. The enclosing
WavAudioSourceis responsible for repositioning itsFileChannel(the only byte-source mode that supports seeking) todataStartByteOffset + frameIndex * bytesPerFramebefore 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- ifframeIndexis out of range
-
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 viaseekToFrame(long).
-
totalFrames
public long totalFrames()- Returns:
- the total number of frames declared by the WAV header
(cached from
WaveFormat.totalFrames()). Reaching this value makes subsequentreadFrames(double[], int, int)calls return-1.
-