// 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.*;
import java.nio.file.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.*;
import javax.sound.sampled.AudioSystem;
import net.avadeaux.klipspringer.*;
import net.avadeaux.klipspringer.codec.*;
import org.json.*;
public class TrackSplitter {
/** Segment that corresponds to one track after splitting.*/
public static class Segment {
private long startFrame; // assigned from Detector.Track
private long endFrame; // assigned from Detector.Track
private long inFrames = 0; // fade-in length, before startFrame
private long outFrames = 0; // fade-out length, after endFrame
private long postFrames; // post-track silence length
private MixSpec.Fade inFade = null; // function for fade-in, null if inFrames==0
private MixSpec.Fade outFade = null; // function for fade-out, null if outFrames==0
private Segment() { }
private Segment(Detector.Track tr) {
this();
startFrame = tr.startFrame;
endFrame = tr.endFrame;
}
/** Gets subsegment index: -1 if before the segment, 0 if in fade-in, 1 if in mid part, 2 if
* in fade-out, 3 if in post-track silence, and 4 if after segment.
*/
public int sub(long frame) {
return frame < endFrame
? (frame < startFrame
? (frame < startFrame-inFrames
? -1
: 0)
: 1)
: (frame < endFrame+outFrames
? 2
: (frame < endFrame+outFrames+postFrames
? 3
: 4));
}
public long lo(int sub) {
switch (sub) {
case 0: return startFrame - inFrames;
case 1: return startFrame;
case 2: return endFrame;
case 3: return endFrame + outFrames;
case 4: return endFrame + outFrames + postFrames;
default: throw new IndexOutOfBoundsException("not applicable: "+sub);
}
}
public long hi(int sub) {
switch (sub) {
case -1: return startFrame - inFrames;
case 0: return startFrame;
case 1: return endFrame;
case 2: return endFrame + outFrames;
case 3: return endFrame + outFrames + postFrames;
default: throw new IndexOutOfBoundsException("not applicable: "+sub);
}
}
public double dt(int sub) {
switch (sub) {
case 0: return 1.0/(inFrames-1);
case 2: return 1.0/(outFrames-1);
default: throw new IndexOutOfBoundsException("not applicable: "+sub);
}
}
public int fade(int sub, double t, int value) {
switch (sub) {
case 0: return inFade.mix(0, value, t);
case 2: return outFade.mix(value, 0, t);
default: throw new IndexOutOfBoundsException("not applicable: "+sub);
}
}
}
public static class Hop implements Comparable<Hop> {
private long pos = 0, length = 0;
public long pos() { return pos; }
public long length() { return length; }
public Hop(Segment seg, long pos, long length) {
set(seg, seg.lo(0)+pos, length);
}
public void set(Segment seg, long posDiff, long lengthDiff) {
long p = pos + posDiff;
long l = length + lengthDiff;
if (l < 0 || p < seg.lo(1) || p+l >= seg.hi(1)) {
throw new CommandException("hop limits outside mid subsection of track");
}
pos = p;
length = l;
}
public int compareTo(Hop that) {
return pos < that.pos
? -1
: (pos > that.pos ? 1 : 0);
}
}
private static class CommandException extends RuntimeException {
private final String text;
CommandException(String text) { this.text = text; }
public String toString() { return text; }
}
private final static Pattern trackPointP = Pattern.compile("(\\d+)(?::(\\d+))?");
private final static String helpText;
static {
helpText = "Commands:%n"
+" l list%n"
+" p [<track>[:sect] [<off_time> [<dur_time>]]] play%n"
+" q quit%n"
+" w [<track> [<to_track>]] write%n"
+" v value set%n"
+" <no argument> list current values%n"
+" fading <true|false> toggle playing with fading%n"
+" silence <fraction> level considered silence%n"
+" track <seconds> minimum track time%n"
+" gap <seconds> minimum gap time%n"
+" itime <seconds> default fade-in time%n"
+" otime <seconds> default fade-out time%n"
+" ifade <fadespec> default fade-in type%n"
+" ofade <fadespec> default fade-out type%n"
+" name <name> base file name for write%n"
+String.format(" type <%-38swrite file type%%n", String.join("|", EncodeFile.formats)+">")
+" m <track> <start|end|in|out> <d_time> modify track time%n"
+" e <time> modify final post-track time%n"
+" f <track> <in|out> <fadespec> change in/out fader of track%n"
+" d <track> delete track%n"
+" j <track> join track with predecessor%n"
+" r [<track>] rescan for silence%n"
+" c <track> <off_time> cut, new track at time%n"
+" h new <track> <pos_time> <length_time> hop past length at position%n"
+" drop <hop_no> drop the hop%n"
+" <hop_no> <p|l> time modify hop position or length%n"
+" s save current segment state%n"
+" o load saved segment state%n"
+" ? print this message%n"
+"TTY key commands during playback:%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"
+" f Fading on/off%n"
+" p Save position%n"
+" r Restore position%n"
+" q Quit%n";
}
private ArrayList<Hop> hops = new ArrayList<Hop>();
private Track input = null;
private Segment[] segs = null;
private Console cons = System.console();
// Values set by command-line arguments and/or commands.
private double silenceLevel = 0.0032;
private double minTrackSecs = 5;
private double minGapSecs = 0.5;
private int secAtoms = 75; // red book frame is 1/75 second
private double inSecs = 0.1;
private double outSecs = 2.0;
private double postSecs = 2.0;
private MixSpec.Fade inFade = parseFade("exp,8");
private MixSpec.Fade outFade = parseFade("lin_pow");
private String output = null;
private double obufsec = 1.0;
private int urunreco = 2;
private double flushSilenceSecs = 0.2;
private Path dir = Paths.get("");
private boolean quiet = false;
private boolean fading = true;
private String baseFileName = "tracksplit";
private String wtype = "flac";
// Set after secAtoms.
private int atomFrames;
private long totalFrames; // truncated to whole atoms
private String htformat, mtformat, stformat;
private Pattern timeP;
public TrackSplitter(Track input) {
this.input = input;
if (input != null) { setSecAtoms(secAtoms); }
}
private static JSONArray jsonArray(Segment[] segs, ArrayList<Hop> hops) {
JSONArray arr = new JSONArray(segs.length+hops.size());
for (Segment s : segs) {
JSONObject obj = new JSONObject();
obj.put("start", s.startFrame);
obj.put("end", s.endFrame);
obj.put("in", s.inFrames);
obj.put("out", s.outFrames);
obj.put("post", s.postFrames);
obj.put("inFade", s.inFade == null ? JSONObject.NULL : s.inFade.toString());
obj.put("outFade", s.outFade == null ? JSONObject.NULL : s.outFade.toString());
arr.put(obj);
}
for (Hop h : hops) {
JSONObject obj = new JSONObject();
obj.put("hop", true);
int ix = segmentIndex(segs, h.pos);
obj.put("track", ix+1);
obj.put("pos", h.pos - segs[ix].lo(0));
obj.put("length", h.length);
arr.put(obj);
}
return arr;
}
private Hop[] sortedHops() {
Hop[] a = new Hop[hops.size()];
hops.toArray(a);
Arrays.sort(a);
return a;
}
public void load(Path path) throws IOException {
JSONArray arr = new JSONArray(Files.readString(path));
int isegs = arr.length();
while (isegs > 0 && arr.getJSONObject(isegs-1).has("hop")) { isegs--; }
Segment[] nsegs = new Segment[isegs];
for (int i = 0; i < nsegs.length; i++) {
JSONObject obj = arr.getJSONObject(i);
Segment s = nsegs[i] = new Segment();
s.startFrame = obj.getLong("start");
s.endFrame = obj.getLong("end");
s.inFrames = obj.getLong("in");
s.outFrames = obj.getLong("out");
s.postFrames = obj.getLong("post");
Object inFade = obj.get("inFade");
s.inFade = inFade instanceof String ? parseFade((String) inFade) : null;
Object outFade = obj.get("outFade");
s.outFade = outFade instanceof String ? parseFade((String) outFade) : null;
}
for (int i = 0; i < nsegs.length; i++) {
Segment s = nsegs[i];
if (i == 0) {
if (s.lo(0) < 0) { throw new IllegalArgumentException("invalid segment state"); }
} else {
if (s.lo(0) != nsegs[i-1].hi(3)) { throw new IllegalArgumentException("invalid segment state"); }
}
for (int j = 0; j < 4; j++) {
if (s.hi(j) < s.lo(j)) { throw new IllegalArgumentException("invalid segment state"); }
}
if (nsegs[nsegs.length-1].hi(3) > totalFrames) { throw new IllegalArgumentException("invalid segment state"); }
}
segs = nsegs;
for (int i = isegs; i < arr.length(); i++) {
JSONObject obj = arr.getJSONObject(i);
hops.add(new Hop(segs[obj.getInt("track")-1], obj.getLong("pos"), obj.getLong("length")));
}
}
public void setSecAtoms(int secAtoms) {
this.secAtoms = secAtoms;
atomFrames = input.format().rate()/secAtoms;
if (atomFrames*secAtoms != input.format().rate()) {
System.err.println("-secAtoms "+secAtoms+" does not divide sample rate "+input.format().rate());
System.exit(2);
}
totalFrames = input.frames()/atomFrames*atomFrames;
int cdigs = Integer.toString(secAtoms-1).length();
htformat = "%d:%02d:%02d'%0"+cdigs+"d";
mtformat = "%d:%02d'%0"+cdigs+"d";
stformat = "%d'%0"+cdigs+"d";
timeP = Pattern.compile("([+\\-])?(\\d+)(?::(\\d\\d))?(?::(\\d\\d))?(?:'(\\d{"+cdigs+"}))?");
}
// Gets subsegment lengths in an arry for convenience in v argument processing.
private long[] getLengths(int tno) {
Segment s = segs[tno-1];
long[] sl = new long[5];
sl[0] = tno == 1 // before-gap
? s.lo(0)
: segs[tno-2].postFrames;
sl[1] = s.hi(0) - s.lo(0); // fade in
sl[2] = s.hi(1) - s.lo(1); // mid section
sl[3] = s.hi(2) - s.lo(2); // fade out
sl[4] = tno < segs.length // after-gap
? s.hi(3) - s.lo(3)
: totalFrames - s.lo(3);
return sl;
}
private void setLengths(int tno, long[] sl) {
for (long f : sl) {
if (f < 0) { throw new CommandException("out of range"); }
}
Segment s = segs[tno-1];
long preFrames = sl[0];
if (tno > 1) {
segs[tno-2].postFrames = preFrames;
preFrames = segs[tno-2].hi(3);
}
s.inFrames = sl[1];
s.startFrame = preFrames + s.inFrames;
s.endFrame = s.startFrame + sl[2];
s.outFrames = sl[3];
if (tno < segs.length) { s.postFrames = sl[4]; }
}
private String timeString(long f) {
long c = f*secAtoms/input.format().rate();
if (c > 60*60*secAtoms) {
return String.format(htformat,
c/(60*60*secAtoms),
c%(60*60*secAtoms)/(60*secAtoms),
c%(60*secAtoms)/secAtoms,
c%secAtoms);
} else if (c > 60*secAtoms) {
return String.format(mtformat,
c/(60*secAtoms),
c%(60*secAtoms)/secAtoms,
c%secAtoms);
} else {
return String.format(stformat,
c/secAtoms,
c%secAtoms);
}
}
private long timeAtoms(String timeString) {
Matcher m = timeP.matcher(timeString);
if (!m.matches()) { throw new CommandException("invalid time"); }
int secs;
if (m.group(4) != null) {
secs = Integer.parseInt(m.group(2))*60*60
+ Integer.parseInt(m.group(3))*60
+ Integer.parseInt(m.group(4));
} else if (m.group(3) != null) {
secs = Integer.parseInt(m.group(2))*60
+ Integer.parseInt(m.group(3));
} else {
secs = Integer.parseInt(m.group(2));
}
int atoms = m.group(5) == null
? 0
: Integer.parseInt(m.group(5));
if (atoms >= secAtoms) { throw new CommandException("atom count exceeded in time string"); }
long t = secs*secAtoms + atoms;
return "-".equals(m.group(1)) ? -t : t;
}
public static int segmentIndex(Segment[] segs, long f) {
int lo = 0, hi = segs.length-1;
while (lo < hi) {
int mid = (lo+hi)/2;
if (f < segs[mid].lo(0)) { hi = mid; }
else if (f < segs[mid].hi(3)) { return mid; }
else { lo = mid+1; }
}
return lo;
}
public static Segment segment(Segment[] segs, long f) { return segs[segmentIndex(segs, f)]; }
private void parseArgs(String[] args) throws IOException {
String helpText = "Arguments: [options] audiofile\n\n"
+"Options:\n"
+"-s <float> Silence level, 0 to 1. Default is "+silenceLevel+"\n"
+"-t <float> Minimum track length in seconds. Default is "+minTrackSecs+"\n"
+"-g <float> Minimum gap length in seconds. Default is "+minGapSecs+"\n"
+"-secatoms <int> Atomic time units per second. Default is "+secAtoms+"\n"
+"-output <string> PCM output device or URL. Default is standard output device\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"
+"-flushsec <float> Extra silence to flush output on playback. Default is "+flushSilenceSecs+"\n"
+"-dir <path> Directory for w, s, and o commands. Default is cwd\n"
+"-q Suppress output during playback\n"
+"-help or --help Print this message\n"
+"-- No more options, next argument is audio file\n";
int argPos = 0;
int secAtomsSet = secAtoms;
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 ("-s" .equals(o)) { silenceLevel = Double.parseDouble(v); }
else if ("-t" .equals(o)) { minTrackSecs = Double.parseDouble(v); }
else if ("-g" .equals(o)) { minGapSecs = Double.parseDouble(v); }
else if ("-secatoms" .equals(o)) { secAtomsSet = Integer.parseInt(v); }
else if ("-output" .equals(o)) { output = v; }
else if ("-obufsec" .equals(o)) { obufsec = Double.parseDouble(v); }
else if ("-urunreco" .equals(o)) { urunreco = Integer.parseInt(v); }
else if ("-flushsec" .equals(o)) { flushSilenceSecs = Double.parseDouble(v); }
else if ("-dir" .equals(o)) { dir = Paths.get(v); }
else if (o.startsWith("-h") || o.startsWith("--h")) { System.out.println(helpText); System.exit(0); }
else if (o.length() == 0) {
System.err.println("Empty file 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;
}
}
if (argPos != args.length-1) {
System.err.println("Not single file name argument (use -h for usage)");
System.exit(2);
}
input = Track.single(args[argPos]);
baseFileName = Track.fileNameStem(Path.of(args[argPos]).getFileName().toString());
setSecAtoms(secAtomsSet);
}
private static MixSpec.Fade parseFade(String spec) {
String[] s = spec.split(" *, *");
double[] params = new double[s.length-1];
for (int i = 0; i < params.length; i++) {
params[i] = Double.parseDouble(s[i+1]);
}
return MixSpec.fade(s[0], params);
}
private Segment[] detect(long from, long to) throws IOException {
Detector det = new Detector(input.format(), atomFrames, silenceLevel, minTrackSecs, minGapSecs, from, to);
input.decode(from, det);
Detector.Track[] result = det.result();
Segment[] dsegs = new Segment[result.length];
long iFrames = (long) Math.round(inSecs*secAtoms) * atomFrames;
long oFrames = (long) Math.round(outSecs*secAtoms) * atomFrames;
for (int i = 0; i < result.length; i++) {
dsegs[i] = new Segment(result[i]);
dsegs[i].inFade = inFade;
dsegs[i].outFade = outFade;
if (i == 0) {
dsegs[i].inFrames = Math.min(iFrames, dsegs[0].startFrame - from);
} else {
dsegs[i-1].postFrames -= dsegs[i].inFrames = Math.min(iFrames, dsegs[i-1].postFrames);
}
long gf = i < result.length-1
? result[i+1].startFrame - result[i].endFrame
: Math.min(to - result[i].endFrame, oFrames + (long) Math.round(postSecs*secAtoms)*atomFrames);
dsegs[i].outFrames = gf > iFrames
? Math.min(oFrames, gf-iFrames)
: gf/atomFrames/2*atomFrames;
dsegs[i].postFrames = gf - dsegs[i].outFrames;
}
return dsegs;
}
/** Track numbers start at 1, 0 means redo the whole input. */
public void detect(int trackNo) throws IOException {
if (trackNo == 0) {
segs = detect(0, totalFrames);
} else {
Segment[] dsegs = detect(segs[trackNo-1].lo(1), segs[trackNo-1].hi(1));
if (dsegs.length < 2) { return; }
if (dsegs[0].startFrame != segs[trackNo-1].startFrame) { throw new IllegalStateException("detection anomaly"); }
dsegs[0].inFrames = segs[trackNo-1].inFrames;
dsegs[0].inFade = segs[trackNo-1].inFade;
dsegs[dsegs.length-1].endFrame = segs[trackNo-1].endFrame;
dsegs[dsegs.length-1].outFrames = segs[trackNo-1].outFrames;
dsegs[dsegs.length-1].outFade = segs[trackNo-1].outFade;
dsegs[dsegs.length-1].postFrames = segs[trackNo-1].postFrames;
Segment[] nsegs = new Segment[segs.length-1+dsegs.length];
System.arraycopy(segs, 0, nsegs, 0, trackNo-1);
System.arraycopy(dsegs, 0, nsegs, trackNo-1, dsegs.length);
System.arraycopy(segs, trackNo, nsegs, trackNo-1+dsegs.length, segs.length-trackNo);
segs = nsegs;
}
}
public void cut(int trackNo, long offFrames) {
Segment s = segs[trackNo-1];
long f = s.lo(0) + offFrames;
if (f <= s.lo(1) || f >= s.hi(1)) { throw new IllegalArgumentException("cutting position must be mid-track"); }
Segment [] nsegs = new Segment[segs.length+1];
System.arraycopy(segs, 0, nsegs, 0, trackNo);
System.arraycopy(segs, trackNo, nsegs, trackNo+1, segs.length-trackNo);
segs = nsegs;
Segment z = segs[trackNo] = new Segment();
z.startFrame = f;
z.endFrame = segs[trackNo-1].endFrame;
z.outFrames = segs[trackNo-1].outFrames;
z.postFrames = segs[trackNo-1].postFrames;
z.inFade = inFade;
z.outFade = segs[trackNo-1].outFade;
segs[trackNo-1].endFrame = f;
segs[trackNo-1].outFrames = 0;
segs[trackNo-1].postFrames = 0;
segs[trackNo-1].outFade = outFade;
}
public void list(PrintWriter out) {
for (int i = 0; i < segs.length; i++) {
Segment s = segs[i];
out.println((i+1)+". "
+(s.lo(0) < s.hi(0) ? s.inFade+" "+timeString(s.hi(0)-s.lo(0))+" | " : "| ")
+ timeString(s.lo(1))+" - "+timeString(s.hi(1))
+(s.lo(2) < s.hi(2) ? " | " + s.outFade+" "+timeString(s.hi(2)-s.lo(2)) : " |")
+" | "+timeString(s.hi(3)-s.lo(3))
+" ("+timeString(s.hi(3)-s.lo(0))+")");
}
out.println("trailing: "+timeString(totalFrames-segs[segs.length-1].hi(3)));
for (int i = 0; i < hops.size(); i++) {
Hop h = hops.get(i);
int tix = segmentIndex(segs, h.pos);
Segment s = segs[tix];
out.println("hop "+i+": track "+(tix+1)+" pos "+timeString(h.pos-s.lo(0))+", length "+timeString(h.length));
}
}
public void play(long startFrame, long endFrame, Device.Player.Factory outFact) throws IOException {
if (startFrame < 0 || startFrame >= totalFrames) {
throw new IllegalArgumentException("play starting point out of range");
}
if (endFrame != AudioSystem.NOT_SPECIFIED && (endFrame < startFrame || endFrame > totalFrames)) {
throw new IllegalArgumentException("play endpoint out of range");
}
OutputSpec.Notify noti = new OutputSpec.Notify();
if (outFact == null) {
outFact = OutputSpec.process(output, obufsec, urunreco, noti);
}
PlayProcess proc = new PlayProcess(input, segs, sortedHops(), endFrame, fading, flushSilenceSecs);
TrackTerminalDisplay display = quiet ? null : new TrackTerminalDisplay(proc);
TrackKeyInterface intf = new TrackKeyInterface(proc, outFact, proc, segs.length-1,
(double) (startFrame-segs[0].lo(0))/input.format().rate());
proc.seek(startFrame, outFact);
new Thread("PlayProcess decode") {
public void run() {
proc.run();
if (display != null) { display.quit(); }
intf.quit();
}
}.start();
try {
noti.verifyReady();
if (display != null) { new Thread(display, "Terminal display").start(); }
intf.runUntilQuit();
proc.quit();
if (display != null) { 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);
}
}
public void write(int lo, int hi) throws IOException {
for (int i = lo-1; i < hi; i++) {
String fnam = Path.of(String.format("%02d.%s%s.%s",
i+1,
baseFileName,
"raw".equals(wtype) ? Track.rawParamQualityString(input.format()) : "",
wtype)).toString();
cons.printf("writing %s%n", fnam);
PlayProcess proc = new PlayProcess(input, segs, sortedHops(), segs[i].hi(3), true, 0);
proc.seek(segs[i].lo(0), OutputSpec.file(fnam, segs[i].hi(3)-segs[i].lo(0), null));
proc.run();
}
}
public int tracks() { return segs.length; }
private int trackNo(String[] a, int i) {
if (a.length <= i) { throw new CommandException("missing track number"); }
int tno = -1;
try { tno = Integer.parseInt(a[i]); } catch (NumberFormatException ex) { }
if (tno < 1 || tno > segs.length) { throw new CommandException("invalid track number"); }
return tno;
}
private int optTrackNo(String[] a, int i) {
if (a.length <= i) { return 0; }
int tno = -1;
try { tno = Integer.parseInt(a[i]); } catch (NumberFormatException ex) { }
if (tno < 0 || tno > segs.length) { throw new CommandException("invalid track number"); }
return tno;
}
private long optTrackPoint(String[] a, int i) {
if (a.length <= i) { return 0; }
Matcher m = trackPointP.matcher(a[i]);
if (!m.matches()) { throw new CommandException("invalid p position"); }
int tno = Integer.parseInt(m.group(1));
if (m.group(2) == null) {
return tno == 0 ? 0 : segs[tno-1].lo(0);
} else {
if (tno == 0) { throw new CommandException("invalid p position"); }
return segs[tno-1].lo(Integer.parseInt(m.group(2)));
}
}
private long timeAtoms(String[] a, int i) {
if (a.length <= i) { return 0; }
return timeAtoms(a[i]);
}
private void tooLong(String[] a, int i) {
if (a.length > i) { throw new CommandException("too many arguments"); }
}
private boolean isArg(String[] a, int i, String val) {
return i < a.length && a[i].equals(val);
}
private boolean bool(String[] a, int i) {
if (i >= a.length) { throw new CommandException("missing boolean argument"); }
return Boolean.parseBoolean(a[i]);
}
private String arg(String[] a, int i) {
if (i >= a.length) { throw new CommandException("missing argument"); }
return a[i];
}
private double floating(String[] a, int i) {
if (i >= a.length) { throw new CommandException("missing floating-point argument"); }
return Double.parseDouble(a[i]);
}
private String fileType(String s) {
for (String f : EncodeFile.formats) {
if (s.equals(f)) { return s; }
}
throw new CommandException("invalid file type");
}
private int hopIx(String arg) {
int i = Integer.parseInt(arg);
if (i < 0 || i >= hops.size()) {
throw new CommandException("hop no out of range");
}
return i;
}
public void commandLoop() {
if (cons == null) {
System.err.println("No console");
if (System.getProperty("klipspringer.debug") != null) { new Error().printStackTrace(); }
System.exit(1);
}
try {
Terminal.enableInterruptGetChar();
} catch (IOException ex) {
System.out.println("failed to enable input interrupt: "+ex);
if (System.getProperty("klipspringer.debug") != null) { ex.printStackTrace(); }
System.exit(1);
}
final String qh = " (? for help)%n";
commandloop: while (true) {
try {
String line = null;
try { line = cons.readLine("klipspit> "); } catch (RuntimeException ex) { }
if (line == null) { break commandloop; }
line = line.trim();
if (line.length() == 0) { continue; }
String[] a = line.split("\\s+");
if (a[0].length() != 1) { cons.printf("invalid command: %s"+qh, a[0]); continue; }
switch (a[0].charAt(0)) {
case 'l':
list(cons.writer());
break;
case 'p': {
long point = optTrackPoint(a, 1);
long offAtoms = timeAtoms(a, 2);
long durAtoms = timeAtoms(a, 3);
tooLong(a, 4);
if (durAtoms < 0) { cons.printf("negative duration"+qh); break; }
long startFrame = point + offAtoms*atomFrames;
play(startFrame, durAtoms == 0 ? AudioSystem.NOT_SPECIFIED : startFrame+durAtoms*atomFrames, null);
break;
}
case 'q':
break commandloop;
case 'w': {
int lo = optTrackNo(a, 1), hi;
if (lo == 0) {
tooLong(a, 2);
lo = 1;
hi = segs.length;
} else {
hi = optTrackNo(a, 2);
tooLong(a, 3);
if (hi == 0) { hi = lo; }
}
write(lo, hi);
break;
}
case 'v': {
tooLong(a, 3);
if (a.length == 1) { showSettings(); }
else if (isArg(a, 1, "fading")) { fading = bool(a, 2); }
else if (isArg(a, 1, "silence")) { silenceLevel = floating(a, 2); }
else if (isArg(a, 1, "track")) { minTrackSecs = floating(a, 2); }
else if (isArg(a, 1, "gap")) { minGapSecs = floating(a, 2); }
else if (isArg(a, 1, "itime")) { inSecs = floating(a, 2); }
else if (isArg(a, 1, "otime")) { outSecs = floating(a, 2); }
else if (isArg(a, 1, "ifade")) { inFade = parseFade(arg(a, 2)); }
else if (isArg(a, 1, "ofade")) { outFade = parseFade(arg(a, 2)); }
else if (isArg(a, 1, "name")) { baseFileName = arg(a, 2); }
else if (isArg(a, 1, "type")) { wtype = fileType(arg(a, 2)); }
else { cons.printf("invalid v argument"+qh); }
break;
}
case 'm': {
int tno = trackNo(a, 1);
long[] sl = getLengths(tno);
String p = arg(a, 2);
long t = timeAtoms(a, 3)*atomFrames;
tooLong(a, 4);
Segment s = segs[tno-1];
int sub, gap;
if ("start".equals(p)) { sl[0] += t; sl[2] -= t; }
else if ("end".equals(p)) { sl[2] += t; sl[4] -= t; }
else if ("in".equals(p)) { sl[0] -= t; sl[1] += t; }
else if ("out".equals(p)) { sl[3] += t; sl[4] -= t; }
else { cons.printf("invalid m argument"+qh); }
setLengths(tno, sl);
break;
}
case 'e': {
tooLong(a, 2);
Segment s = segs[segs.length-1];
long t = s.postFrames + timeAtoms(a, 1)*atomFrames;
if (t < 0) { cons.printf("negative duration"+qh); }
if (s.lo(3)+t > totalFrames) { cons.printf("beyond end of input"+qh); }
else { s.postFrames = t; }
break;
}
case 'f': {
Segment s = segs[trackNo(a, 1)-1];
String p = arg(a, 2);
MixSpec.Fade f = parseFade(arg(a, 3));
tooLong(a, 4);
if ("in".equals(p)) { s.inFade = f; }
else if ("out".equals(p)) { s.outFade = f; }
else { cons.printf("invalid f argument"+qh); }
break;
}
case 'h': {
String arg = arg(a, 1);
if ("new".equals(arg)) {
hops.add(new Hop(segs[trackNo(a, 2)-1], timeAtoms(a, 3)*atomFrames, timeAtoms(a, 4)*atomFrames));
tooLong(a, 5);
break;
}
if ("drop".equals(arg)) {
int i = hopIx(arg(a, 2));
tooLong(a, 3);
hops.remove(i);
break;
}
Hop h = hops.get(hopIx(arg));
arg = arg(a, 2);
long t = timeAtoms(a, 3)*atomFrames;
Segment s = segs[segmentIndex(segs, h.pos)];
tooLong(a, 4);
if ("p".equals(arg)) { h.set(s, t, 0); }
else if ("l".equals(arg)) { h.set(s, 0, t); }
else { cons.printf("invalid h argument, expect p or l: %s%n", arg); }
break;
}
case 'j': {
int tno = trackNo(a, 1);
tooLong(a, 2);
if (tno == 1) { cons.printf("attempted to join first track"+qh); }
segs[tno-2].endFrame = segs[tno-1].endFrame;
segs[tno-2].outFrames = segs[tno-1].outFrames;
segs[tno-2].postFrames = segs[tno-1].postFrames;
Segment[] nsegs = new Segment[segs.length-1];
System.arraycopy(segs, 0, nsegs, 0, tno-1);
System.arraycopy(segs, tno, nsegs, tno-1, segs.length-tno);
segs = nsegs;
break;
}
case 'd': {
int tno = trackNo(a, 1);
tooLong(a, 2);
if (tno > 1) { segs[tno-2].postFrames += segs[tno-1].hi(3) - segs[tno-1].lo(0); }
Segment[] nsegs = new Segment[segs.length-1];
System.arraycopy(segs, 0, nsegs, 0, tno-1);
System.arraycopy(segs, tno, nsegs, tno-1, segs.length-tno);
segs = nsegs;
break;
}
case 'r': {
int tno = optTrackNo(a, 1);
tooLong(a, 2);
detect(tno);
break;
}
case 'c': {
int tno = trackNo(a, 1);
long off = timeAtoms(a, 2)*atomFrames;
tooLong(a, 3);
cut(tno, off);
break;
}
case 's': {
Path path = saveFilePath();
try (Writer w = Files.newBufferedWriter(path)) {
jsonArray(segs, hops).write(w);
cons.printf("saved segment state to %s%n", path);
}
break;
}
case 'o': {
Path path = saveFilePath();
load(saveFilePath());
cons.printf("loaded segment state from %s%n", path);
break;
}
case '?':
cons.printf(helpText);
break;
default:
cons.printf("invalid command"+qh);
break;
}
} catch (Exception ex) {
cons.printf(ex.toString()+qh);
if (System.getProperty("klipspringer.debug") != null) { ex.printStackTrace(); }
}
}
}
private Path saveFilePath() {
return Path.of(dir.toString(), String.format("%s.json", baseFileName));
}
private void showSettings() {
cons.printf("fading: %b%n", fading);
cons.printf("silence level: %f%n", silenceLevel);
cons.printf("min track seconds: %f%n", minTrackSecs);
cons.printf("min gap seconds: %f%n", minGapSecs);
cons.printf("fade-in seconds: %f%n", inSecs);
cons.printf("fade-out seconds: %f%n", outSecs);
cons.printf("fade-in spec: %s%n", inFade);
cons.printf("fade-out spec: %s%n", outFade);
cons.printf("file name stem: %s%n", baseFileName);
cons.printf("output file type: %s%n", wtype);
}
public static void main(String[] args) {
TrackSplitter splitter = new TrackSplitter(null);
try {
splitter.parseArgs(args);
try {
Path path = splitter.saveFilePath();
splitter.load(path);
splitter.cons.printf("Loaded saved segment state from %s%n", path);
} catch (NoSuchFileException ex) {
splitter.cons.printf("Detecting using preconfigured values%n");
splitter.detect(0);
}
splitter.commandLoop();
} catch (Throwable th) {
System.err.println(th);
if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
System.exit(1);
}
}
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home