Skip to content

Commit

Permalink
Allow to adjust playback-speed of the TarsosDSPPlayer
Browse files Browse the repository at this point in the history
TarsosDSP actually supports this by adjusting parameters
of the wsola implementation.

This way we can avoid stopping/restarting the audio-player
and can simply adjust playback speed at runtime.
  • Loading branch information
centic9 committed Sep 10, 2024
1 parent c5f848b commit d6635df
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 25 deletions.
37 changes: 35 additions & 2 deletions src/main/java/org/dstadler/audio/example/AudioWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class AudioWriter implements Runnable {
private ClearablePipedInputStream in;
private PlayerThread player;

private String options = "";

public AudioWriter(SeekableRingBuffer<Chunk> buffer, Runnable stopper, BooleanSupplier shouldStop) throws IOException {
this.buffer = buffer;
this.stopper = stopper;
Expand All @@ -48,9 +50,9 @@ private synchronized void createPipe() throws IOException {
@Override
public void run() {
try {
//player.setOptions("");

player = new PlayerThread(in, stopper);
player.setOptions(options);

Thread playerThread = new Thread(player, "Player thread");
playerThread.setDaemon(true);
playerThread.start();
Expand Down Expand Up @@ -92,6 +94,15 @@ private long writeLoop() throws IOException {
return chunks;
}

/**
* Allows to empty out current buffers.
*
* This is usually used to remove chunks in the queue so a shutdown
* is performed quickly.
*
* @throws IOException If clearing buffers or re-creating internal
* data-structures fails
*/
public void clearBuffer() throws IOException {
in.clearBuffer();

Expand All @@ -105,4 +116,26 @@ public void clearBuffer() throws IOException {
player.triggerRestart(in);
}
}

/**
* Allow to set speed of audio playback for the current
* implementation of the audio player.
*
* If the player is already running, the
* changed speed is passed to the
* current instance.
*
* If the used implementation of the audio player does not
* support tempo-adjustment, nothing happens.
*
* @param tempo The adjusted tempo in terms of multiplay of real-time playback,
* 1.0f for real-time playback, e.g. 1.5f for fairly fast playback
*/
public void setTempo(float tempo) {
if (player != null) {
player.setOptions(Float.toString(tempo));
}

options = Float.toString(tempo);
}
}
23 changes: 19 additions & 4 deletions src/main/java/org/dstadler/audio/example/ExamplePlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
public class ExamplePlayer {
private final static Logger log = LoggerFactory.make();

// test-options, set to true to enable some features below
private final static boolean SEEK = false;
private final static boolean TEMPO_ADJUST = false;

private static volatile boolean shouldStop = false;

@SuppressForbidden(reason = "Uses System.exit()")
Expand Down Expand Up @@ -47,7 +51,8 @@ private static void run(String url) throws IOException, InterruptedException {
Thread writer = new Thread(audioWriter, "Writer thread");
writer.start();

//int seeked = -1;
int seeked = -1;
int count = 0;

// then read and populate the buffer until we have read everything
while (!buffer.empty() && !shouldStop) {
Expand All @@ -62,13 +67,23 @@ private static void run(String url) throws IOException, InterruptedException {

Thread.sleep(1000);

/* just for testing seeking
if (seeked == -1) {
// just for testing seeking
if (SEEK && seeked == -1) {
seeked = seek(buffer, audioWriter, 0.95);
}*/
}

// for testing adjust tempo on the fly
if (TEMPO_ADJUST && count % 5 == 0) {
// for testing use tempo between 0.6 and 1.5
float tempo = 0.6f + ((count % 4) * 0.3f);
log.info("Adjusting tempo to " + tempo);
audioWriter.setTempo(tempo);
}
} catch (/*IOException |*/ InterruptedException e) {
log.log(Level.WARNING, "Caught unexpected exception", e);
}

count++;
}

// indicate that no more data is read and thus playing should stop
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/dstadler/audio/example/PlayerThread.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class PlayerThread implements Runnable {
private final Runnable stopper;
private volatile boolean restart = true;
private AudioPlayer player;
private String options = "";

public PlayerThread(PipedInputStream in, Runnable stopper) {
// some Audio classes try to use mark()/reset(), thus we use a wrapping BufferedInputStream()
Expand All @@ -43,6 +44,7 @@ public void run() {
restart = false;

player = createPlayer(inputStream);
player.setOptions(options);

//player.setOptions("");

Expand Down Expand Up @@ -73,4 +75,23 @@ private AudioPlayer createPlayer(InputStream inputStream) {
// return new AudioSPIPlayer(inputStream);
// return new JLayerPlayer(inputStream);
}

/**
* Allow to set custom options for the current
* implementation of the audio player.
*
* If the player is already running, the
* changed options are passed to the
* current instance.
*
* @param options The options to set, possible values
* depend on the used player implementation
*/
public void setOptions(String options) {
if (player != null) {
player.setOptions(options);
}

this.options = options;
}
}
20 changes: 17 additions & 3 deletions src/main/java/org/dstadler/audio/player/AudioPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@
* of audio playback functionality.
*
* The implementations will usually receive a link to the
* audio-data as part construction, the methods in this
* audio-data during construction, the methods in this
* interface are just used to start and stop playing
* via play() and close()
* via play() and close().
*
* Some implementations may support setting options,
* both initially and at runtime to adjust audio playback
*/
public interface AudioPlayer extends AutoCloseable {

/**
* Allows to set options which are usually
* specific for the current implementation.
*
* Some implementations also allow to adjust
* options at runtime by calling this method
* with changed options while the player keeps
* playing audio.
*
* @param options A string with options specific
* for the current implementation
*/
void setOptions(String options);

/**
Expand Down
40 changes: 24 additions & 16 deletions src/main/java/org/dstadler/audio/player/TarsosDSPPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class TarsosDSPPlayer implements AudioPlayer {
private AudioDispatcher dispatcher;
private float tempo = 1.0f;

private WaveformSimilarityBasedOverlapAdd wsola;
private float sampleRate;

public TarsosDSPPlayer(InputStream stream) {
this.stream = stream;
}
Expand All @@ -37,6 +40,12 @@ public void setOptions(String options) {
Preconditions.checkState(tempo > 0,
"Cannot play at speed zero or less, but had: %s",
tempo);

// if already playing, pass on the new parameters to allow to change tempo at runtime
if (wsola != null) {
wsola.setDispatcher(dispatcher);
wsola.setParameters(WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults(tempo, sampleRate));
}
}
}

Expand All @@ -52,25 +61,22 @@ public void play() throws IOException, UnsupportedAudioFileException {
// transform the stream to mono as TarsosDSP can only process Mono currently
AudioInputStream monoStream = AudioUtils.convertToMono(ain);

// if we should speed up or slow down audio playback, add the WSOLA time stretcher
if(tempo != 1.0f) {
AudioFormat monoFormat = monoStream.getFormat();
TarsosDSPAudioInputStream audioStream = new JVMAudioInputStream(monoStream);

// then define the time stretching step
WaveformSimilarityBasedOverlapAdd wsola = new WaveformSimilarityBasedOverlapAdd(
WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults(tempo, monoFormat.getSampleRate()));
// in order to be able to speed up or slow down audio playback, add the WSOLA time stretcher
AudioFormat monoFormat = monoStream.getFormat();

// then start up the TarsosDSP audio system
TarsosDSPAudioInputStream audioStream = new JVMAudioInputStream(monoStream);
dispatcher = new AudioDispatcher(audioStream,
wsola.getInputBufferSize() * monoFormat.getChannels(),
wsola.getOverlap() * monoFormat.getChannels());
// then define the time stretching step
sampleRate = monoFormat.getSampleRate();
wsola = new WaveformSimilarityBasedOverlapAdd(
WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults(tempo, sampleRate));

dispatcher.addAudioProcessor(wsola);
} else {
TarsosDSPAudioInputStream audioStream = new JVMAudioInputStream(monoStream);
dispatcher = new AudioDispatcher(audioStream, 1024, 512);
}
// then start up the TarsosDSP audio system
dispatcher = new AudioDispatcher(audioStream,
wsola.getInputBufferSize() * monoFormat.getChannels(),
wsola.getOverlap() * monoFormat.getChannels());

dispatcher.addAudioProcessor(wsola);

// the audio-output processor provides the actual audio playback in the pipeline
dispatcher.addAudioProcessor(new be.tarsos.dsp.io.jvm.AudioPlayer(dispatcher.getFormat()));
Expand Down Expand Up @@ -98,5 +104,7 @@ public void close() throws IOException {
if(stream != null) {
stream.close();
}

wsola = null;
}
}

0 comments on commit d6635df

Please sign in to comment.