// 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.IOException;
import java.io.Writer;
import net.avadeaux.klipspringer.codec.*;

/** Feeder runnable that encodes one track at a time, and runs consumer to recieve the data in a
  * separate thread..a
  */
public class TrackFeeder implements TrackKeyInterface.Control {
    private final Track.List tracks;                    // all tracks
    private final PlayState state;
    private final TrackConsumer consumer;               // where consumer and feeder threads meet

    private Track nextTrack = null;                     // next track to play
    private long framesIntoNextTrack = 0;               // where in next track to start playing
    private Device.Player.Factory nextFact = null;      // writer factory for next track
    private boolean seeking = false;                    // set when continuous play is interrupted
    private boolean monomix = false;                    // set when monomix requested, active on next seek
    private boolean exit = false;                       // set when consumer thread exits

    public TrackFeeder(Track.List tracks, PlayState state, TrackConsumer consumer) {
        this.tracks = tracks;
        this.state = state;
        this.consumer = consumer;
    }

    /** Feeder thread operation. */
    public void run() throws IOException {
        new Thread("Audio Consumer") {
            public void run() {
                try {
                    consumer.runConsumerThread();
                } finally {
                    synchronized (TrackFeeder.this) {
                        exit = true;
                        TrackFeeder.this.notifyAll();
                    }
                }
            }
        }.start();

        while (true) {
            Track tr;
            long off;
            Device.Player.Factory fact;
            synchronized (this) {
                // Wait for something to do or exit.
                while (!exit && (nextTrack == null || nextFact == null)) {
                    try { wait(); } catch (InterruptedException e) { }
                }
                if (exit) { return; }

                // Reset consumer from stop.
                consumer.readyPlay();

                // Get track to decode.
                tr = nextTrack;
                off = framesIntoNextTrack;
                fact = nextFact;

                // Set the default for next round to be the following track.
                nextTrack = tracks.ofIndex(tr.index + 1);
                framesIntoNextTrack = 0;

                // Start or continue consuming.
                if (seeking) {
                    if (!consumer.startPlay(tr, off, fact, monomix)) {
                        tr = null;                      // abandoned due to seek
                    }
                    seeking = false;
                } else {
                    if (!consumer.continuePlay(tr, fact)) {
                        tr = null;                      // end of stream
                        nextTrack = null;
                    }
                }
            }
            // Decode until end of track.
            if (tr != null) { tr.decode(off, consumer); }
        }
    }

    /** Extra arguments: Query.Onoff monomix (default: DETECT), Writer jsonWriter (default: null). */
    public void seek(int trackIx, double time, Device.Player.Factory outFact, Object... extra) throws IOException {
        Query.Onoff monomix = extra.length > 0 ? (Query.Onoff) extra[0] : Query.Onoff.DETECT;
        Writer w = extra.length > 1 ? (Writer) extra[1] : null;
        consumer.stop();
        synchronized (this) {
            seeking = true;
            if (monomix != Query.Onoff.DETECT) {
                this.monomix = monomix == Query.Onoff.ON;
            }
            Track tr = tracks.ofIndex(trackIx);
            if (tr == null) { throw new IllegalArgumentException("No track for index "+trackIx); }
            double ti = tr.startTimeSecs() + time;
            nextTrack = tracks.ofTime(ti);
            ti = Math.max(0, ti - nextTrack.startTimeSecs());
            framesIntoNextTrack = (long) (ti * nextTrack.format().getSampleRate());
            if (framesIntoNextTrack < nextTrack.frames()) { // seek only inside of track
                nextFact = outFact;                         // null if streaming and seeka
            } else {
                // If seeking somehow went beyond the end of the track, don't do the seek but
                // instead indicate to the client that playback is at the end of the case. Right now
                // I'm actually not sure why (it shouldn't happen anyway), but it could be the
                // solution for an unusual error case, so I'm leaving it for now.
                nextFact = null;
                ti = nextTrack.frames() / nextTrack.format().getSampleRate();
            }
            if (w != null) { w.append(",\"track\":"+nextTrack.index+",\"time\": "+ti); }
            notifyAll();
        }
    }

    public void pause(Device.Player.Factory resumeFact) throws IOException {
        try {
            consumer.pause();
        } catch (PlayState.ResumeException ex) {
            synchronized (this) {
                nextTrack = ex.track;
                framesIntoNextTrack = (long) (ex.time * ex.track.format().getSampleRate());
                nextFact = resumeFact;
                seeking = true;
                notifyAll();
            }
        }
    }

    public void unpause() throws IOException { consumer.unpause(); }
}

Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home