// 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;
import java.io.*;
import java.nio.file.Path;
import java.util.Arrays;
import org.json.JSONArray;
import net.avadeaux.klipspringer.codec.*;
/**
* Player class with main method.
*/
public class TrackPlayer {
private String output = null;
private int xbufbytes = 262144;
private double obufsec = 2.0;
private int urunreco = 2;
private Query.Onoff monomix = Query.Onoff.OFF;
private String controlsock = null;
private int controlthreads = 20;
private int trackoff = 1, starttrack = 1;
private double startseek = 0;
private boolean cmdintf = true;
private boolean quiet = false;
private JSONArray mixSpecJson = null;
private OutputSpec.Notify noti = new OutputSpec.Notify();
void run(String[] args) {
String helpText = "Arguments: [options] audiofiles\n\n"
+"Options:\n"
+"-output <string> PCM output device or URL. Default is standard output device\n"
+"-xbufbytes <int> Minimum internal PCM buffer size. Default is "+xbufbytes+"\n"
+"-obufsec <float> External buffer time on PCM output. Default is "+obufsec+"\n"
+"-urunreco <int> Times to attempt recovery after buffer underrun. Default is "+urunreco+"\n"
+"-controlsock <path> Socket path for control server. By default, there is no control server\n"
+"-controlthreads <int> Max threads in control server. Default is "+controlthreads+"\n"
+"-cmdintf <boolean> Use interactive command interface when applicable. Default is "+cmdintf+"\n"
+"-q Suppress TTY output\n"
+"-mixjson <data> Set mixing data for tracks. Default is no mixing.\n"
+"-monomix <on|off> Set all audio channels to be mixed equal. Default is "+monomix.toString().toLowerCase()+"\n"
+"-trackoff <int> Number of first track. Default is "+trackoff+"\n"
+"-starttrack <int> File to start playing from. Default is "+starttrack+"\n"
+"-seek <float> Start playing number of seconds into the track\n"
+"-help or --help Print this message\n"
+"-- Remaining arguments are file names\n\n"
+"TTY key commands:\n"
+" SPACE Pause/play\n"
+" < Skip track backwards\n"
+" > Skip track ahead\n"
+" UP ARROW Skip 30 seconds ahead\n"
+" DOWN ARROW Skip 30 seconds back\n"
+" RIGHT ARROW Skip 5 seconds ahead\n"
+" LEFT ARROW Skip 5 seconds back\n"
+" m Monomix on/off\n"
+" p Save position\n"
+" r Restore position\n"
+" q Quit\n";
if (args.length == 0) {
System.err.println(helpText);
System.exit(2);
}
int argPos = 0;
while (argPos < args.length) {
String o = args[argPos];
if (o.charAt(0) != '-') { break; }
argPos++;
if ("--" .equals(o)) { break; }
else if ("-q" .equals(o)) { quiet = true; continue; }
String v = argPos < args.length ? args[argPos++] : "";
if ("-output" .equals(o)) { output = v; }
else if ("-xbufbytes" .equals(o)) { xbufbytes = Integer.parseInt(v); }
else if ("-obufsec" .equals(o)) { obufsec = Double.parseDouble(v); }
else if ("-urunreco" .equals(o)) { urunreco = Integer.parseInt(v); }
else if ("-controlsock" .equals(o)) { controlsock = v; }
else if ("-controlthreads" .equals(o)) { controlthreads = Integer.parseInt(v); }
else if ("-cmdintf" .equals(o)) { cmdintf = Boolean.parseBoolean(v); }
else if ("-mixjson" .equals(o)) { mixSpecJson = new JSONArray(v); }
else if ("-monomix" .equals(o)) { monomix = Query.Onoff.valueOf(v.toUpperCase()); }
else if ("-trackoff" .equals(o)) { trackoff = Integer.parseInt(v); }
else if ("-starttrack" .equals(o)) { starttrack = Integer.parseInt(v); }
else if ("-seek" .equals(o)) { startseek = Double.parseDouble(v); }
else if (o.startsWith("-h") || o.startsWith("--h")) { System.out.println(helpText); return; }
else if (o.length() == 0) {
System.err.println("Empty files argument (use -help for usage)");
System.exit(2);
} else if (o.charAt(0) == '-') {
System.err.println("Unregognized option "+o+" (use -help for usage)");
System.exit(2);
} else {
break;
}
}
String[] fnams = Arrays.copyOfRange(args, argPos, args.length);
MixSpec[] mixs = new MixSpec[fnams.length];
if (mixSpecJson == null) {
Arrays.fill(mixs, MixSpec.getInstance());
} else {
for (int i = 0; i < fnams.length; i++) {
mixs[i] = MixSpec.getInstance(mixSpecJson.getJSONObject(i));
}
}
Track.List tracks = new Track.List(fnams, mixs, trackoff);
PlayState state = new PlayState(tracks);
TrackTerminalDisplay display = null;
QueueExecutor streamKeepAliveExecutor = new QueueExecutor("Stream keep-alive");
try {
Device.Player.Factory outFact = OutputSpec.process(output, obufsec, urunreco, noti);
TrackConsumer consumer = new TrackConsumer(tracks, state, xbufbytes, outFact == null ? 120000 : 800);
TrackFeeder feeder = new TrackFeeder(tracks, state, consumer);
CommandInterface cmdExec = new TrackCommandInterface(feeder, tracks, state, outFact, streamKeepAliveExecutor, 5000);
if (cmdintf && outFact instanceof OutputSpec.LocalDeviceOutput && Terminal.inTty()) {
TrackKeyInterface intf = new TrackKeyInterface(feeder, outFact, state, tracks.length-1);
TrackTerminalDisplay d = display;
Thread t = new Thread("Interactive Interface") {
public void run() {
try {
intf.runUntilQuit();
} catch (Throwable th) {
if (d != null && Terminal.errTty()) { d.quit(); }
System.err.println(th);
if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
System.exit(1);
}
System.exit(0);
}
};
t.setDaemon(true);
t.start();
}
if (!quiet && outFact instanceof OutputSpec.LocalDeviceOutput && Terminal.outTty()) {
display = new TrackTerminalDisplay(state);
new Thread(display, "Terminal display").start();
}
if (outFact != null) { feeder.seek(starttrack-trackoff, startseek, outFact, monomix, null); }
if (controlsock != null && controlsock.length() > 0) {
Path sock = Path.of(controlsock);
AsyncCommandServer cmdServ = new AsyncCommandServer(cmdExec, sock, controlthreads);
Thread cmdThread = new Thread("Control Server") {
public void run() {
noti.verifyReady();
System.out.println("Ready control socket "+sock);
cmdServ.run();
}
};
cmdThread.setDaemon(true);
cmdThread.start();
}
feeder.run();
if (display != null && Terminal.errTty()) { display.quit(); }
} catch (Throwable th) {
if (display != null && Terminal.errTty()) { display.quit(); }
System.err.println(th);
if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
System.exit(1);
} finally {
streamKeepAliveExecutor.exit();
}
}
public static void main(String[] args) {
try {
new TrackPlayer().run(args);
} catch (Throwable th) {
th.printStackTrace();
System.exit(1);
}
}
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home