// 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.*;
/** Channel for writing PCM data, with quality-compatible conversion. */
public class PcmChannelEncoder implements PcmWriter {
private final WritableByteChannel channel;
private final ByteBuffer buf;
private final PcmBuffer fbuf;
private final PcmFormat inFormat, outFormat;
/** General constructor. Note that there are factory methods {@link ofWav(PcmFormat,
* WritableByteChannel, long)} and {@link ofWav(PcmFormat, WritableByteChannel, long)}
*/
public PcmChannelEncoder(PcmFormat inFormat, WritableByteChannel channel, PcmFormat outFormat, Device.BitLayout outLout) {
if (!inFormat.matchesQuality(outFormat)) {
throw new IllegalArgumentException("Format "+inFormat+" not compaticle with "+outFormat);
}
this.channel = channel;
this.inFormat = inFormat;
this.outFormat = outFormat;
buf = ByteBuffer.allocateDirect(2048*outFormat.fs());
fbuf = PcmBuffer.of(buf, outFormat, outLout);
}
public boolean write(ByteBuffer data) throws IOException {
int ifs = inFormat.fs(), ofs = outFormat.fs();
int p = data.position(), q = data.limit();
while (p < q) {
int m = Math.min((q-p)/ifs, buf.capacity()/ofs); // max samples for this iteration
data.limit(p + m*ifs);
buf.clear();
fbuf.put(data, inFormat);
buf.flip();
int w = channel.write(buf)/ofs; // samples written
p += w*ifs;
if (w < m) { // channel took less than requested
data.position(p); // back to show what was output
data.limit(q); // reset to original
return false;
}
}
return true;
}
public void close() throws IOException { channel.close(); }
/** Factory method that gets an encoder for producing raw PCM format output. */
public static PcmChannelEncoder ofRaw(PcmFormat inFormat, WritableByteChannel channel, PcmFormat outFormat) {
return new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.LSB);
}
/** Factory method that gets an encoder for producing PCM format output with a WAV header. */
public static PcmChannelEncoder ofWav(PcmFormat inFormat, WritableByteChannel channel, long totalFrames) throws IOException {
PcmFormat outFormat =
new PcmFormat(inFormat.rate(), inFormat.bips(), inFormat.channels(),
inFormat.bips() > 8, // 8-bit is unsigned
(inFormat.bips()+7)/8*inFormat.channels(), // no byte padding
ByteOrder.LITTLE_ENDIAN,
inFormat.significantBips());
ByteBuffer hb = ByteBuffer.allocate(44);
hb.order(ByteOrder.LITTLE_ENDIAN);
PcmChannelEncoder pchan;
if (totalFrames == AudioSystem.NOT_SPECIFIED) {
// This means that the file size is not known until close, and the channel must be
// seekable to get back and fill in the size.
if (!(channel instanceof SeekableByteChannel)) {
throw new IllegalArgumentException("Size not predetermined and output channel not seekable");
}
totalFrames = 0; // write zero in header for now
pchan = new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.MSB) {
public void close() throws IOException {
if (channel.isOpen()) {
SeekableByteChannel seekable = (SeekableByteChannel) channel;
long size = seekable.size()-44;
if (size+36 > 0xffffffffL) {
System.err.println("Output exceedes maximum WAV size, header will indicate shorter than actual");
hb.putInt(4, 0xffffffff);
hb.putInt(40, 0xffffffff);
} else {
hb.putInt(4, (int) (size+36));
hb.putInt(40, (int) size);
}
seekable.position(0);
hb.position(0);
hb.limit(44);
while (hb.hasRemaining()) { channel.write(hb); }
channel.close();
}
}
};
} else {
pchan = new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.MSB);
}
long size = totalFrames*outFormat.fs();
if (size+36 > 0xffffffffL) { throw new IOException("Size too large for WAV: "+size); }
int bips = outFormat.significantBips();
if (bips+7 < outFormat.bips() || bips <= 8 && outFormat.bips() > 8) {
// In these strange cases (not likely to ever arise), we need to lie about the
// precision, because the encoding is different for the scaled-up bit depth.
bips = outFormat.bips();
}
hb.putInt(0x46464952); // 0: "RIFF"
hb.putInt((int) size + 36); // 4
hb.putLong(0x20746d6645564157L); // 8: "WAVEfmt "
hb.putInt(16); // 16: PCM
hb.putShort((short) 1); // 20: PCM = 1 (lin. quant.)
hb.putShort((short) outFormat.channels()); // 22
hb.putInt(outFormat.rate()); // 24
hb.putInt(outFormat.rate() * outFormat.fs()); // 28
hb.putShort((short) outFormat.fs()); // 32
hb.putShort((short) bips); // 34
hb.putInt(0x61746164); // 36: "data"
hb.putInt((int) size); // 40
hb.flip();
while (hb.hasRemaining()) { pchan.channel.write(hb); }
return pchan;
}
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home