All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dstadler.audio.example.ExamplePlayer Maven / Gradle / Ivy

package org.dstadler.audio.example;

import org.dstadler.audio.buffer.Chunk;
import org.dstadler.audio.buffer.RangeDownloadingBuffer;
import org.dstadler.audio.player.AudioPlayer;
import org.dstadler.audio.player.AudioSPIPlayer;
import org.dstadler.audio.util.ClearablePipedInputStream;
import org.dstadler.commons.logging.jdk.LoggerFactory;
import org.dstadler.commons.util.SuppressForbidden;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.dstadler.audio.buffer.Chunk.CHUNK_SIZE;

/**
 * A simple commandline audio player to show usage
 * of some of the components that are contained in this project.
 *
 * You can specify a filename, a local file-url or a remote URL of a
 * download-able audio-stream.
 */
public class ExamplePlayer {
    private final static Logger log = LoggerFactory.make();

    private static volatile boolean shouldStop = false;

    @SuppressForbidden(reason = "Uses System.exit()")
    public static void main(String[] args) throws IOException, InterruptedException {
        if (args.length != 1) {
            System.err.println("Usage: ExamplePlayer ");
            System.exit(1);
        }

        LoggerFactory.initLogging();

        run(args[0]);
    }

    private static void run(String url) throws IOException, InterruptedException {
        log.info("Playing file " + url);

        // the ring-buffer handles decoupling of downloading the audio and playing it
        // it will buffer up to chunkSize*bufferedChunks bytes in memory and will
        // download more data whenever needed
        RangeDownloadingBuffer buffer = new RangeDownloadingBuffer(url, "", null, 1000,
                Chunk.CHUNK_SIZE, p -> null);

        // play audio in a separate thread
        AudioWriter audioWriter = new AudioWriter(buffer);
        Thread writer = new Thread(audioWriter, "Writer thread");
        writer.start();

        //int seeked = -1;

        // then read and populate the buffer until we have read everything
        while (!buffer.empty() && !shouldStop) {
            try {
                // this just ensures that we fill the buffer in parallel
                // to the writer thread so that playing audio never runs
                // out of data to play
                int fetched = buffer.fillupBuffer(15, 50);
                if (fetched > 0) {
                    log.info("Downloaded " + fetched + " chunks, having buffer: " + buffer);
                }

                Thread.sleep(1000);

                /* just for testing seeking
                if (seeked == -1) {
                    seeked = seek(buffer, audioWriter, 0.95);
                }*/
            } catch (/*IOException |*/ InterruptedException e) {
                log.log(Level.WARNING, "Caught unexpected exception", e);
            }
        }

        // indicate that no more data is read and thus playing should stop
        shouldStop = true;
        writer.join();
    }

    @SuppressWarnings("unused")
    private static int seek(RangeDownloadingBuffer buffer, AudioWriter audioWriter, double percentage) throws IOException {
        int availableChunks = buffer.fill();
        int availableBackwardChunks = availableChunks - buffer.size();

        // compute where we need to seek to based on the available number of chunks
        // and the current read-position
        double nrOfChunks = percentage * availableChunks;
        int nrOfChunksRounded = (int)(percentage * availableChunks);

        // when we need to seek backwards, this will become negative
        final int chunksToSeek = nrOfChunksRounded - availableBackwardChunks;

        log.info("Clearing piped-buffer");
        audioWriter.clearBuffer();

        // ask the buffer to seek that man chunks forward or backwards
        int seeked = buffer.seek(chunksToSeek);

        log.info("Seeking " + seeked + " chunks, had request of " + chunksToSeek + " and " + nrOfChunksRounded +
                "/" + nrOfChunks + " chunks because of percentage " + percentage + " and available chunks: " + availableChunks +
                " and available backwards: " + availableBackwardChunks + ": " + buffer);
        return seeked;
    }

    /**
     * A thread which fetches data from the buffer and populates
     * a PipedInputStream that is always filled to let the actual
     * audio-player take data whenever needed.
     */
    private static class AudioWriter implements Runnable {
        private final RangeDownloadingBuffer buffer;
        private final PipedOutputStream out;
        private final ClearablePipedInputStream in;

        public AudioWriter(RangeDownloadingBuffer buffer) throws IOException {
            this.buffer = buffer;
            this.out = new PipedOutputStream();

            // configure enough buffer for a few chunks in the pipe to avoid flaky sound output
            in = new ClearablePipedInputStream(out, 5 * CHUNK_SIZE);
        }

        @Override
        public void run() {
            try {
                //player.setOptions("");

                Thread playerThread = new Thread(new PlayerThread(in), "Player thread");
                playerThread.setDaemon(true);
                playerThread.start();

                long chunks = writeLoop();

                log.info("Stopping playing after " + chunks + " chunks");

                // wait for all data to be read by the playing thread
                in.waitAllConsumed();
            } catch (IOException | InterruptedException e) {
                log.log(Level.WARNING, "Caught unexpected exception", e);
            }
        }

        private long writeLoop() throws IOException {
            long chunks = 0;
            while (!shouldStop) {
                // get the next chunk from the buffer, this might block if no more data is
                // available
                Chunk chunk = buffer.next();
                if (chunk == null) {
                    log.info("Buffer is closed, maybe we reached the end of the stream?");
                    break;
                }

                log.fine("Write chunk " + chunk + " with " + chunk.getData().length + " bytes");

                // pass on the chunk to the stream for playing
                out.write(chunk.getData());

                chunks++;
                if (chunks % 200 == 0) {
                    log.info("Writing " + chunk.size() + " bytes, having chunk number: " + chunks);
                }
            }
            return chunks;
        }

        public void clearBuffer() throws IOException {
            in.clearBuffer();
        }
    }

    /**
     * The audio-system in Java needs a thread for actually
     * running the player
     */
    private static class PlayerThread implements Runnable {
        private final InputStream inputStream;

        public PlayerThread(PipedInputStream in) {
            // some Audio classes try to use mark()/reset(), thus we use a wrapping BufferedInputStream()
            // here to provide this functionality as PipedInputStream does not support it
            this.inputStream = new BufferedInputStream(in, CHUNK_SIZE);
        }

        @Override
        public void run() {
            try {
                AudioPlayer player = createPlayer(inputStream);

                //player.setOptions("");

                player.play();
            } catch (Throwable e) {
                log.log(Level.WARNING, "Caught unexpected exception", e);

                shouldStop = true;
            }
        }

        private AudioPlayer createPlayer(InputStream inputStream) throws IOException {
            // any of the implementations will play .mp3 streams
            // the SPI-based one can play OggVorbis as well
//        return new TarsosDSPPlayer(inputStream);
            return new AudioSPIPlayer(inputStream);
//        return new JLayerPlayer(inputStream);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy