// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.
package net.avadeaux.klipspringer.codec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.*;
import java.io.IOException;
import javax.sound.sampled.*;
/** Audio decoder that reads PCM data from a channel. Does not actually perform any decoding, but
* copies data verbatim from the channel. This is useful for wrapping input of raw PCM data, for
* instance from a WAV file with {@link #ofWav(SeekableByteChannel, AudioDecoder.Target) ofWav} or
* a raw file by {@link #ofRaw(ReadableByteChannel, PcmFormat, long, AudioDecoder.Target) ofRaw}.
*/
public class PcmChannelDecoder implements AudioDecoder {
private enum State { NEW, INITIALIZED, METADATA_DECODED, AUDIO_DECODED, DESTROYED }
/** To delegate obtaining metadata to subclass. */
static class Metadata {
final PcmFormat format;
final long totalFrames;
final long dataPosition;
/** Stores metadata and file position of PCM data. */
Metadata(PcmFormat format, long totalFrames, long dataPosition) {
this.format = format;
this.totalFrames = totalFrames;
this.dataPosition = dataPosition;
}
}
private final ReadableByteChannel channel;
private final Target target;
private final Metadata metadata;
private State state = State.NEW;
PcmChannelDecoder(ReadableByteChannel channel, Metadata metadata, Target target) {
this.channel = channel;
this.metadata = metadata;
this.target = target;
state = State.INITIALIZED;
}
/** Returns true if the underlying channel is seekable. */
public boolean reusable() {
return channel instanceof SeekableByteChannel && state != State.DESTROYED;
}
public synchronized void close() throws IOException {
state = State.DESTROYED;
channel.close();
}
// Rethrows exception after closing.
private void fail(RuntimeException ex) throws IOException {
try { close(); } catch (Throwable th) {
if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
}
throw ex;
}
// Rethrows exception after closing.
private void fail(IOException ex) throws IOException {
try { close(); } catch (Throwable th) {
if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
}
throw ex;
}
public synchronized void decodeMetadata() throws IOException {
try {
if (state != State.INITIALIZED) {
throw new IllegalStateException("Attempted decodeMetadata in state "+state);
}
target.metadata(metadata.format, metadata.totalFrames);
state = State.METADATA_DECODED;
} catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
}
public synchronized boolean decodeAll(long fromSampleNo) throws IOException {
try {
if (state == State.INITIALIZED) {
decodeMetadata();
}
if (state == State.NEW || state == State.DESTROYED) {
throw new IllegalStateException("Attempted decodeAll in state "+state);
}
ByteBuffer bb = ByteBuffer.allocateDirect(1024*metadata.format.fs());
bb.order(metadata.format.order());
if (state != State.METADATA_DECODED || fromSampleNo > 0) {
((SeekableByteChannel) channel).position(metadata.dataPosition + fromSampleNo*metadata.format.fs());
}
long bytesRemaining = metadata.totalFrames == AudioSystem.NOT_SPECIFIED
? -1
: (metadata.totalFrames-fromSampleNo)*metadata.format.fs();
while (bytesRemaining != 0) {
bb.clear();
if (bytesRemaining > 0 && bytesRemaining < bb.capacity()) { bb.limit((int) bytesRemaining); }
int n = channel.read(bb);
if (n < 0) {
if (bytesRemaining < 0) { break; } // total bytes not specified, fine
throw new IOException("Input truncated, missing bytes: "+bytesRemaining);
}
if (bytesRemaining > 0) { bytesRemaining -= n; }
bb.flip();
if (!target.write(bb)) { state = State.AUDIO_DECODED; return false; }
bb.clear();
}
state = State.AUDIO_DECODED;
return true;
} catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
return false; // unreachable
}
/** Factory method that gets a decoder for reading raw PCM format data. */
public static PcmChannelDecoder ofRaw(ReadableByteChannel channel, PcmFormat format, long totalFrames, Target target) {
return new PcmChannelDecoder(channel, new Metadata(format, totalFrames, 0), target);
}
private static void readHeaderSection(ReadableByteChannel channel, ByteBuffer hb, int size, String what) throws IOException {
hb.position(0);
hb.limit(size);
channel.read(hb);
if (hb.hasRemaining()) { throw new UnsupportedFormatException("Failed to read "+what); }
hb.flip();
}
private static Metadata readWavMetadata(SeekableByteChannel seekable) throws IOException {
ByteBuffer hb = ByteBuffer.allocate(40);
hb.order(ByteOrder.LITTLE_ENDIAN);
// Read chunk identifier.
long chunkSize;
for (long chunkPos = 0;;) {
readHeaderSection(seekable, hb, 12, "RIFF chunk header");
int riff = hb.getInt();
chunkSize = (long) hb.getInt() & 0xffffffff;
int wave = hb.getInt();
if (riff == 0x46464952 && wave == 0x45564157) { break; } // "RIFF" "WAVE"
chunkPos += 8 + chunkSize;
seekable.position(chunkPos);
}
// Read "fmt " bloc.
while (true) {
readHeaderSection(seekable, hb, 8, "bloc");
int blocId = hb.getInt();
long blocSize = (long) hb.getInt() & 0xffffffff;
if (blocId == 0x20746d66) { // found "fmt "
if (blocSize < 16 || blocSize > 40) { throw new UnsupportedFormatException("Unsupported size of fmt bloc: "+blocSize); }
readHeaderSection(seekable, hb, (int) blocSize, "fmt bloc");
break;
}
seekable.position(seekable.position() + blocSize); // ignore and read next bloc
}
short audioFormat = hb.getShort();
if (audioFormat != 1 && audioFormat != -2) { throw new UnsupportedFormatException("Unsupported audio format value: "+audioFormat); }
int channels = hb.getShort();
int rate = hb.getInt();
int bypsec = hb.getInt(); // rate * fs
int fs = hb.getShort();
if (bypsec != rate * fs) { throw new UnsupportedFormatException("Inconsistent rate: "+rate+" frames/s, "+fs+" bytes/frame, "+bypsec+"bytes/s"); }
int bips = hb.getShort();
if (audioFormat == -2) { // WAVE_FORMAT_EXTENSIBLE
if (hb.remaining() < 2) { throw new UnsupportedFormatException("Extensible WAV format header too small"); }
// With header extension, the number of bits specified first should match the frame
// size, and the number of valid bits is the first field in the extension.
if (hb.getShort() != 0) { // size of extension may be zero
if (bips * channels != fs * 8) { throw new UnsupportedFormatException("Number of bits ("+bips+") does not match frame size ("+fs+")"); }
bips = hb.getShort();
if (bips < 1 || bips > fs/channels*8) { throw new UnsupportedFormatException("Inconsistent valid bits ("+bips+") for container size "+(fs/channels*8)); }
}
}
if (bips == 0) { bips = fs/channels*8; } // allowed in spec. 3.0 (1994)
// Enter "data" bloc
while (true) {
readHeaderSection(seekable, hb, 8, "bloc");
int blocId = hb.getInt();
long blocSize = (long) hb.getInt() & 0xffffffff;
if (blocId == 0x61746164) { // found "data"
// When bips is not a multiple of 8, values are left-aligned, corresponding to
// Device.BitLayout.MSB. Since the AudioDecoder interface does not have the concept
// of bit layouts and assumes values to be LSB aligned, we round up bips to the next
// multiple of 8, and set the significant number of bits to the actual bit depth.
return new Metadata(new PcmFormat(rate, bips+(-bips & 7), channels, bips > 8, fs, ByteOrder.LITTLE_ENDIAN, bips), blocSize/fs, 44);
}
seekable.position(seekable.position() + blocSize); // ignore and read next bloc
}
}
/** Factory method that gets a decoder for reading PCM format data with a WAV header. */
public static PcmChannelDecoder ofWav(SeekableByteChannel seekable, Target target) throws IOException {
return new PcmChannelDecoder(seekable, readWavMetadata(seekable), target);
}
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home