// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.eavadeaux.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.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.Executor;
import javax.sound.sampled.AudioFormat;

/** Audio stream that sends FLAC encoded data to a channel. Also doubles as file encoder class when
  * created via {@link #fileEncoder(String, PcmFormat, int) fileEncoder}.
  */
public class FlacStream extends ExternalEncodedStream {
    static { Library.init(); }

    /** Selects format suitable for FLAC streaming with the same semantics as {@link
      * PcmFormat.Selector#selectFormat(AudioFormat)}.
      */
    public static PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
        int bips = fmt.getSampleSizeInBits();
        int sbips = fmt instanceof PcmFormat ? ((PcmFormat) fmt).significantBips() : fmt.getSampleSizeInBits();
        int channels = fmt.getChannels();
        AudioFormat.Encoding enc = fmt.getEncoding();
        boolean signed = enc == AudioFormat.Encoding.PCM_SIGNED;
        int fs = fmt.getFrameSize();
        int rfs = channels*(1 << (bips-1)/8 - bips/25);
        ByteOrder order = fmt.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
        ByteOrder rorder = ByteOrder.nativeOrder();
        if (signed && order == rorder && fs == rfs) { return PcmFormat.of(fmt); }
        if (!signed && enc != AudioFormat.Encoding.PCM_UNSIGNED) {
            throw new UnsupportedFormatException("Non-PCM format not supported:"+fmt);
        }
        return new PcmFormat((int) fmt.getSampleRate(), bips, channels, true, rfs, rorder, sbips);
    }

    /** The bit layout for PCM data sent to a FLAC stream. */
    public final static Device.BitLayout BIT_LAYOUT = Device.BitLayout.LSBX;

    /** True if OGG FLAC is supported. This is set to false if libFLAC is subject to
      * <a href="https://github.com/xiph/flac/issues/661">an issue</a> that renders the number of
      * samples in an encoding chunk unknown.
      */
    public final static boolean oggSupported = oggSupported();

    /** Creates a FLAC stream with the given specifications and destination channel.
      *
      * <p>The <code>holdingBytes</code> parameter is used when streaming to recipient that does not
      * begin to process the input until it has accumulated a certain amount of compressed data,
      * causing the recipient to potentially stall when receiving highly compressible sound (such as
      * silence). In particular, web browsers sometimes do this. When streaming to a web browser, a
      * reasonable value to prevent stalling is 49152. When the destination is a file or something
      * else that does not process audio in real time, it can be set to 0.
      *
      * <p>If <code>keepAliveExec</code> is non-null, it is used by {@link #close()} to execute a
      * wait loop that terminates when either the recipient has sent callback notification of having
      * played (almost) all the data it has been sent, or no callback is made for
      * <code>keepAliveTimeoutSecs</code> seconds. (See also {@link #drain()}.)
      */
    public FlacStream(PcmFormat format,
                      boolean ogg,
                      int comprLevel,
                      int bufferFrames,
                      double writeaheadSecs,
                      double timeoutSecs,
                      int holdingBytes,
                      WritableByteChannel channel,
                      Executor keepAlivePool,
                      double keepAliveTimeoutSecs)
        throws IOException
    {
        this(null, format, ogg, comprLevel, bufferFrames, writeaheadSecs, timeoutSecs, holdingBytes, channel, keepAlivePool, keepAliveTimeoutSecs);
    }

    // Private constructor with file name parameter. The file name is non-null only when the object
    // is not to be used as a stream, but to encode a file.
    private FlacStream(String fileName,
                       PcmFormat format,
                       boolean ogg,
                       int comprLevel,
                       int bufferFrames,
                       double writeaheadSecs,
                       double timeoutSecs,
                       int holdingBytes,
                       WritableByteChannel channel,
                       Executor keepAlivePool,
                       double keepAliveTimeoutSecs)
        throws IOException
    {
        super(channel, format, bufferFrames, writeaheadSecs, timeoutSecs, holdingBytes, new EncoderRecordFactory() {
                public long create(Receiver receiver, PcmFormat format, int bufferFrames) throws IOException {
                    if (!Library.flacSupported()) {
                        throw new UnsupportedOperationException("Klipspringer library lacks FLAC support");
                    }
                    byte[] fnamUtf8 = null;
                    if (fileName != null) {
                        fnamUtf8 = fileName.getBytes(StandardCharsets.UTF_8);
                        fnamUtf8 = Arrays.copyOf(fnamUtf8, fnamUtf8.length+1);
                    }
                    return FlacStream.create(fnamUtf8, ogg, format.rate(), format.bips(), format.channels(), format.ss(), format.signed(), format.bigend(), comprLevel, bufferFrames, receiver);
                }
            }, keepAlivePool, keepAliveTimeoutSecs);
        if (ogg && fileName == null && !oggSupported) {
            throw new IOException("Ogg FLAC stream not supported due to https://github.com/xiph/flac/issues/661");
        }
    }

    public Device.BitLayout layout() { return BIT_LAYOUT; }

    /** Gets a player that encodes to a file rather than a stream. */
    public static PcmWriter fileEncoder(String fileName, PcmFormat dataFormat, int comprLevel) throws IOException {
        PcmFormat bufFormat = selectFormat(dataFormat);
        return new FlacStream(fileName, bufFormat, fileName.endsWith(".oga"), comprLevel, 2048, 0, 0, 0, null, null, 0)
            .fileEncoder(dataFormat, BIT_LAYOUT);
    }

    // Construction method.
    private native static long create(byte[] fnam,      // null unless called from fileEncoder
                                      boolean ogg,
                                      int rate,
                                      int bips,
                                      int channels,
                                      int ss,
                                      boolean signed,
                                      boolean bigend,
                                      int comprLevel,
                                      int bufferFrames,
                                      Receiver receiver)
        throws IOException;

    // Native methods invoked by superclass.
    native void write(long handle, ByteBuffer data, int pos, int frames) throws IOException;
    native void finish(long handle) throws IOException;
    native void free(long handle);

    // Tests for issue that prevents use of OGG FLAC format.
    private native static boolean oggSupported();
}

Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home