// 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.split;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioSystem;
import net.avadeaux.klipspringer.*;
import net.avadeaux.klipspringer.codec.*;
public class SplitPlayer implements Device.Player {
private final TrackSplitter.Segment[] segs;
private final TrackSplitter.Hop[] hops;
private final PcmFormat dformat; // format of received data
private final long endFrame; // stop playing at this point
private final double flushSilenceSecs; // silence to add after endFrame
protected final Device.Player out;
protected final int ofs; // bytes/frame in out
protected final int dfs; // bytes/frame in received data
private final ByteBuffer zbuf; // buffer of zeros for silent parts
private final ByteBuffer obuf; // intermediate buffer
private final PcmBuffer fbuf; // wraps obuf
private boolean fading; // fading/silence on
protected long frame; // current position
private SplitPlayer(TrackSplitter.Segment[] segs,
TrackSplitter.Hop[] hops,
PcmFormat dformat,
Device.Player out,
Device.BitLayout lout,
long startFrame,
long endFrame,
boolean fading,
double flushSilenceSecs)
{
this.segs = segs;
this.hops = hops;
this.dformat = dformat;
this.endFrame = endFrame;
this.out = out;
ofs = out.format().fs();
dfs = dformat.fs();
zbuf = ByteBuffer.allocateDirect(1024*ofs);
zbuf.order(out.format().order());
obuf = ByteBuffer.allocateDirect(1024*ofs);
obuf.order(out.format().order());
fbuf = PcmBuffer.of(obuf, out.format(), lout);
frame = startFrame;
this.fading = fading;
this.flushSilenceSecs = flushSilenceSecs;
}
public PcmFormat format() { return dformat; } // format visible to write caller
// Copies m samples verbatim from data.
private boolean copy(ByteBuffer data, int m) throws IOException {
int q = data.limit();
boolean ok = true;
while (ok && m > 0) {
int p = data.position();
obuf.clear();
data.limit(p+Math.min(m*dfs, // at most m samples
obuf.capacity()/ofs*dfs)); // do not overfill obuf
fbuf.put(data, dformat);
obuf.flip();
ok = out.write(obuf);
int wf = obuf.position()/ofs; // written frames
data.position(p + wf*dfs);
m -= wf;
frame += wf;
}
data.limit(q);
return ok;
}
public synchronized boolean write(ByteBuffer data) throws IOException {
// Stop at endFrame.
int q = data.limit();
if (endFrame != AudioSystem.NOT_SPECIFIED && frame + data.remaining()/dfs >= endFrame) {
data.limit(data.position() + (int) (endFrame-frame)*dfs);
}
int hopi = 0;
while (hopi < hops.length && hops[hopi].pos()+hops[hopi].length() < frame) { hopi++; }
boolean ok = true; // tentative return value
while (ok) {
int m = data.remaining()/dfs; // frames to write in this chunk
if (hopi < hops.length) {
TrackSplitter.Hop h = hops[hopi];
if (h.pos() > frame) { // hop is later
m = (int) Math.min(m, h.pos() - frame); // stop at h.pos()
} else { // hop now
int d = (int) Math.min(h.length() - (frame-h.pos()), m);
frame += d;
m -= d;
data.position(data.position() + d*dfs);
hopi++;
}
}
if (m == 0) { break; } // write done
if (!fading) { // fading is off, copy verbatim
ok = copy(data, m);
} else {
TrackSplitter.Segment s = TrackSplitter.segment(segs, frame);
int sub = s.sub(frame);
if (sub < 4) { // still among segments
m = (int) Math.min(m, s.hi(sub)-frame); // stop at end of subsegment
}
switch (sub) {
case 1: // mid track, play verbatim
ok = copy(data, m);
break;
case 0: // fade in
case 2: { // fade out
obuf.clear();
m = Math.min(m, obuf.capacity()/ofs); // do not overfill obuf
PcmBuffer fdata = PcmBuffer.of(data, dformat, Device.BitLayout.LSB);
int p = data.position();
double dt = s.dt(sub);
double t = (frame - s.lo(sub))*dt;
for (int i = 0; i < m; i++, t += dt) {
for (int j = 0; j < dformat.channels(); j++) {
fbuf.putSample(s.fade(sub, t, fdata.getSample()));
}
}
obuf.flip();
ok = out.write(obuf);
int wf = m - obuf.remaining()/ofs; // written frames
frame += wf;
data.position(p + wf*dfs);
break;
}
default: { // silence
zbuf.clear();
if (m*ofs < zbuf.capacity()) {
zbuf.limit(m*ofs); // only write m samples
} else {
m = zbuf.capacity()/ofs; // connot write more than zbuf
}
ok = out.write(zbuf);
int wf = zbuf.position()/ofs; // written frames
frame += wf;
data.position(data.position() + wf*dfs);
break;
} }
}
}
if (frame == endFrame && flushSilenceSecs > 0) { // add silence to flush out sound
int z = (int) Math.round(dformat.rate()*ofs*flushSilenceSecs);
while (z > 0) {
zbuf.clear();
zbuf.limit(Math.min(z, zbuf.capacity()));
out.write(zbuf);
z -= zbuf.position();
}
ok = false;
}
data.limit(q);
return ok;
}
synchronized long currentFrame() throws IOException {
return frame - out.bufferedFrames();
}
synchronized void fading(boolean on) { fading = on; }
public int bufferedFrames() throws IOException { return out.bufferedFrames(); }
public void drain() throws IOException { out.drain(); }
public void close() throws IOException { out.close(); }
// Optimized for equal input and output format.
private static class VerbatimPlayer extends SplitPlayer {
VerbatimPlayer(TrackSplitter.Segment[] segs, TrackSplitter.Hop[] hops, Device.Player out, long startFrame, long endFrame, boolean fading, double flushSilenceSecs) {
super(segs, hops, out.format(), out, Device.BitLayout.LSB, startFrame, endFrame, fading, flushSilenceSecs);
}
private boolean copy(ByteBuffer data, int m) throws IOException {
int p = data.position(), q = data.limit();
data.limit(p + m*dfs); // limit to m samples
boolean ok = out.write(data);
frame += (data.position()-p)/dfs; // written frames
data.limit(q);
return ok;
}
}
public static SplitPlayer create(TrackSplitter.Segment[] segs,
TrackSplitter.Hop[] hops,
PcmFormat dformat,
Device.Player out,
Device.BitLayout lout,
long startFrame,
long endFrame,
boolean fading,
double flushSilenceSecs)
{
return dformat.matches(out.format()) && (lout == Device.BitLayout.LSB || dformat.bips() == 8*dformat.ss())
? new VerbatimPlayer(segs, hops, out, startFrame, endFrame, fading, flushSilenceSecs)
: new SplitPlayer(segs, hops, dformat, out, lout, startFrame, endFrame, fading, flushSilenceSecs);
}
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home