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