From d6635df688ac00d0c882a51ea9b4b812b15527ef Mon Sep 17 00:00:00 2001 From: Dominik Stadler Date: Tue, 10 Sep 2024 13:06:57 +0200 Subject: [PATCH] Allow to adjust playback-speed of the TarsosDSPPlayer 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. --- .../dstadler/audio/example/AudioWriter.java | 37 ++++++++++++++++- .../dstadler/audio/example/ExamplePlayer.java | 23 +++++++++-- .../dstadler/audio/example/PlayerThread.java | 21 ++++++++++ .../dstadler/audio/player/AudioPlayer.java | 20 ++++++++-- .../audio/player/TarsosDSPPlayer.java | 40 +++++++++++-------- 5 files changed, 116 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/dstadler/audio/example/AudioWriter.java b/src/main/java/org/dstadler/audio/example/AudioWriter.java index b3414a1..9ccb0f2 100644 --- a/src/main/java/org/dstadler/audio/example/AudioWriter.java +++ b/src/main/java/org/dstadler/audio/example/AudioWriter.java @@ -31,6 +31,8 @@ class AudioWriter implements Runnable { private ClearablePipedInputStream in; private PlayerThread player; + private String options = ""; + public AudioWriter(SeekableRingBuffer buffer, Runnable stopper, BooleanSupplier shouldStop) throws IOException { this.buffer = buffer; this.stopper = stopper; @@ -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(); @@ -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(); @@ -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); + } } diff --git a/src/main/java/org/dstadler/audio/example/ExamplePlayer.java b/src/main/java/org/dstadler/audio/example/ExamplePlayer.java index e30b9a1..bda5617 100644 --- a/src/main/java/org/dstadler/audio/example/ExamplePlayer.java +++ b/src/main/java/org/dstadler/audio/example/ExamplePlayer.java @@ -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()") @@ -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) { @@ -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 diff --git a/src/main/java/org/dstadler/audio/example/PlayerThread.java b/src/main/java/org/dstadler/audio/example/PlayerThread.java index 3d032dd..1ecc2d5 100644 --- a/src/main/java/org/dstadler/audio/example/PlayerThread.java +++ b/src/main/java/org/dstadler/audio/example/PlayerThread.java @@ -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() @@ -43,6 +44,7 @@ public void run() { restart = false; player = createPlayer(inputStream); + player.setOptions(options); //player.setOptions(""); @@ -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; + } } diff --git a/src/main/java/org/dstadler/audio/player/AudioPlayer.java b/src/main/java/org/dstadler/audio/player/AudioPlayer.java index a249e03..96f3aef 100644 --- a/src/main/java/org/dstadler/audio/player/AudioPlayer.java +++ b/src/main/java/org/dstadler/audio/player/AudioPlayer.java @@ -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); /** diff --git a/src/main/java/org/dstadler/audio/player/TarsosDSPPlayer.java b/src/main/java/org/dstadler/audio/player/TarsosDSPPlayer.java index fb7143c..919db96 100644 --- a/src/main/java/org/dstadler/audio/player/TarsosDSPPlayer.java +++ b/src/main/java/org/dstadler/audio/player/TarsosDSPPlayer.java @@ -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; } @@ -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)); + } } } @@ -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())); @@ -98,5 +104,7 @@ public void close() throws IOException { if(stream != null) { stream.close(); } + + wsola = null; } }