// 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