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

org.dstadler.audio.buffer.BlockingSeekableRingBuffer Maven / Gradle / Ivy

There is a newer version: 1.2.1
Show newest version
package org.dstadler.audio.buffer;

import com.google.common.base.Preconditions;
import org.dstadler.audio.stream.Stream;
import org.dstadler.audio.util.RuntimeInterruptedException;
import org.dstadler.commons.util.SuppressForbidden;

import java.io.IOException;

/**
 * Implementation of the {@link SeekableRingBuffer} interface which
 * uses {@link Chunk} as data object to provide byte arrays together
 * with meta-data and timestamps at which the chunk was received.
 *
 * The implementation uses two pointers for next-add-position and
 * next-get-position which cannot use the given number of chunks
 * because of ambiguity in the used ring-buffer algorithm.
 *
 * This is accepted to avoid a more complex implementation which
 * has other limitations and downsides.
 *
 * Seeking currently iterates the number of steps given to use
 * a simple implementation, there may be more efficient ways of
 * implementing this.
 */
public class BlockingSeekableRingBuffer implements SeekableRingBuffer, Persistable {
    private static final byte[] EMPTY = new byte[0];

    private final Chunk[] buffer;

    /**
     * indicates the next position to read,
     * there is no more data to read if nextGet == nextAdd
     * this is always in the range [0, numberOfChunks[
     */
    private int nextGet = 0;

    /**
     * indicates the next position to write,
     * this is always in the range [0, numberOfChunks[
     */
    private int nextAdd = 0;

    private int fill = 0;

    /**
     * This enables breaking the blocking wait in next(),
     * set via calling close()
     */
    private boolean stop = false;

    public BlockingSeekableRingBuffer(int numberOfChunks) {
        Preconditions.checkArgument(numberOfChunks > 0, "Had chunks: %s", numberOfChunks);

        this.buffer = new Chunk[numberOfChunks];

        // initialize buffer with empty chunks
        for(int i = 0;i < numberOfChunks;i++) {
            this.buffer[i] = new Chunk(EMPTY, "", 0);
        }
    }

    /**
     * Constructor for Serialization only, will construct the buffer in the state
     * that it was serialized, i.e. the same chunks and the same positions
     *
     * @param buffer The array of chunks as serialized out before
     * @param nextGet The position for the next get operation
     * @param nextAdd The position for the next add operation
     * @param fill The current fill value
     */
    private BlockingSeekableRingBuffer(Chunk[] buffer, int nextGet, int nextAdd, int fill) {
        this.buffer = buffer;
        this.nextGet = nextGet;
        this.nextAdd = nextAdd;
        this.fill = fill;
    }

    @SuppressForbidden(reason = "Uses Object.notify() on purpose here")
    @Override
    public synchronized void add(Chunk chunk) {
        Preconditions.checkNotNull(chunk);

        buffer[nextAdd] = chunk;

        nextAdd = (nextAdd + 1) % buffer.length;
        if(nextAdd == nextGet) {
            // we are overwriting the next to read, so we need to move nextGet forward as well
            nextGet = (nextGet + 1) % buffer.length;
        }

        // increase fill until we wrapped around at least once
        // so we know when the buffer is filled up with data
        if(fill != (buffer.length - 1)) {
            fill++;
        }

        notify();
    }

    @SuppressForbidden(reason = "Uses Object.wait() on purpose here")
    @Override
    public synchronized Chunk next() {
        // wait until data is available
        while(empty() && !stop) {
            try {
                // waiting leaves the synchronized block so other threads
                // can do work while we wait here
                wait(100);
            } catch (InterruptedException e) {
                throw new RuntimeInterruptedException(e);
            }
        }

        if(stop) {
            return null;
        }

        // fetch item before we increase the pointer
        Chunk chunk = buffer[nextGet];

        nextGet = (nextGet + 1) % buffer.length;

        return chunk;
    }

    @Override
    public synchronized Chunk peek() {
        if(empty() || stop) {
            return null;
        }

        return buffer[nextGet];
    }

    @Override
    public synchronized int seek(int nrOfChunks) {
        // this is a very naive initial implementation which actually loops
        // to step forward/backward as long as possible and counts it's steps
        int stepped = 0;
        if(nrOfChunks > 0) {
            for(int i = 0;i < nrOfChunks;i++) {
                if(!incrementNextGet()) {
                    break;
                }
                stepped++;
            }
        } else if (nrOfChunks < 0) {
            for(int i = 0;i > nrOfChunks;i--) {
                if(!decrementNextGet()) {
                    break;
                }
                stepped--;
            }
        }
        return stepped;
    }

    private boolean incrementNextGet() {
        if(empty()) {
            // cannot increment further
            return false;
        }

        // advance by one
        nextGet = (nextGet + 1) % buffer.length;
        return true;
    }

    private boolean decrementNextGet() {
        if(full()) {
            // cannot increment further
            return false;
        }

        // decrement by one
        if(nextGet == 0) {
            nextGet = buffer.length - 1;
        } else {
            nextGet = nextGet - 1;
        }

        return true;
    }

    @Override
    public synchronized boolean empty() {
        //if head and tail are equal, we are empty
        return nextAdd == nextGet;
    }

    @Override
    public synchronized boolean full() {
        // If tail is ahead of the head by 1, we are full
        return ((nextAdd + 1) % buffer.length) == nextGet;
    }

    @Override
    public synchronized int capacity() {
        // minus one because we cannot use all buffer-elements due
        // to head == tail meaning empty and (head - 1) == tail meaning full
        return buffer.length - 1;
    }

    @Override
    public int size() {
        if(nextAdd >= nextGet) {
            return nextAdd - nextGet;
        } else {
            return buffer.length - (nextGet - nextAdd);
        }
    }

    @Override
    public int fill() {
        return fill;
    }

    @Override
    public synchronized void reset() {
        nextAdd = nextGet;
        fill = 0;
    }

    @Override
    public synchronized int bufferedForward() {
        if(nextAdd >= nextGet) {
            return nextAdd - nextGet;
        } else if (fill == buffer.length - 1) {
            return fill - nextGet + nextAdd + 1;
        } else {
            return fill - nextAdd;
        }
    }

    @Override
    public synchronized int bufferedBackward() {
        if(nextAdd >= nextGet) {
            return fill - nextAdd + nextGet;
        } else {
            return nextGet - nextAdd - 1;
        }
    }

    @Override
    public synchronized void close() {
        stop = true;
    }

    @Override
    public String toString() {
        return "BlockingSeekableRingBuffer{" +
                "numberOfChunks=" + (buffer == null ? "" : buffer.length) +
                ", nextGet=" + nextGet +
                ", nextAdd=" + nextAdd +
                ", stop=" + stop +
                ", capacity=" + capacity() +
                ", size=" + size() +
                ", empty=" + empty() +
                ", full=" + full() +
                '}';
    }

    @Override
    public synchronized BufferPersistenceDTO toPersistence(Stream stream, boolean playing, boolean downloadWhilePaused) {
        return new BufferPersistenceDTO(buffer, nextGet, nextAdd, fill, stream, playing, downloadWhilePaused);
    }

    public static BlockingSeekableRingBuffer fromPersistence(BufferPersistenceDTO dto) throws IOException {
        if(dto.getBuffer() == null) {
            throw new IOException("Could not read buffer from persistent file, having: " + dto);
        }

        return new BlockingSeekableRingBuffer(dto.getBuffer(), dto.getNextGet(), dto.getNextAdd(), dto.getFill());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy