// 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