// 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.*;
/** Audio stream that sends Vorbis encoded data to a channel. Also doubles as file encoder class
* when created via {@link #fileEncoder(String, PcmFormat, float) fileEncoder}.
*/
public class VorbisStream extends ExternalEncodedStream {
static { Library.init(); }
/** Selects format suitable for Vorbis streaming with the same semantics as {@link
* PcmFormat.Selector#selectFormat(AudioFormat)}.
*/
public static PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
int channels = fmt.getChannels();
AudioFormat.Encoding enc = fmt.getEncoding();
boolean signed = enc == AudioFormat.Encoding.PCM_SIGNED;
int fs = fmt.getFrameSize();
int bips = fmt.getSampleSizeInBits();
int sbips = fmt instanceof PcmFormat ? ((PcmFormat) fmt).significantBips() : fmt.getSampleSizeInBits();
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 Vorbis stream. */
public final static Device.BitLayout BIT_LAYOUT = Device.BitLayout.MSB;
private final float quality;
/** Creates an Ogg Vorbis 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 VorbisStream(PcmFormat format,
float quality,
double writeaheadSecs,
double timeoutSecs,
int holdingBytes,
WritableByteChannel channel,
Executor keepAlivePool,
double keepAliveTimeoutSecs)
throws IOException
{
this(null, format, quality, 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 VorbisStream(String fileName,
PcmFormat format,
float quality,
double writeaheadSecs,
double timeoutSecs,
int holdingBytes,
WritableByteChannel channel,
Executor keepAlivePool,
double keepAliveTimeoutSecs)
throws IOException
{
// Vorbis documentation says 1024 is a reasonable number of samples to transmit at a time.
super(channel, format, 1024, writeaheadSecs, timeoutSecs, holdingBytes, new EncoderRecordFactory() {
public long create(Receiver receiver, PcmFormat format, int bufferFrames) throws IOException {
if (!Library.vorbisSupported()) {
throw new UnsupportedOperationException("Klipspringer library lacks Vorbis support");
}
byte[] fnamUtf8 = null;
if (fileName != null) {
fnamUtf8 = fileName.getBytes(StandardCharsets.UTF_8);
fnamUtf8 = Arrays.copyOf(fnamUtf8, fnamUtf8.length+1);
}
return VorbisStream.create(fnamUtf8, format.rate(), format.bips(), format.channels(), format.ss(), quality, receiver);
}
}, keepAlivePool, keepAliveTimeoutSecs);
this.quality = quality;
}
/** The bit layout for PCM data sent to a Vorbis stream. */
public Device.BitLayout layout() { return BIT_LAYOUT; }
/** gets the VBR quality specified for this stream. */
public String quality() {
String q = quality+"\u00a0quality";
return q.startsWith("0.") ? q.substring(1) : q;
}
/** Gets a player that encodes to a file rather than a stream. */
public static PcmWriter fileEncoder(String fileName, PcmFormat dataFormat, float quality) throws IOException {
PcmFormat bufFormat = selectFormat(dataFormat);
return new VorbisStream(fileName, bufFormat, quality, 0, 0, 0, null, null, 0)
.fileEncoder(dataFormat, BIT_LAYOUT);
}
// Construction method.
private native static long create(byte[] fnam, // null unless called from fileEncoder
int rate,
int bips,
int channels,
int ss,
float quality,
Receiver receiver)
throws IOException;
// Native methods invoked by superclass.
native void write(long handle, ByteBuffer data, int pos, int frames);
native void finish(long handle) throws IOException;
native void free(long handle);
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home