/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program 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.
*
* This program 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 this program. If not, see
* ****************************************************************************
*/
package io.github.dsheirer.spectrum;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.sample.OverflowableTransferQueue;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.Iterator;
import java.util.LinkedList;
public class OverflowableBufferStream extends OverflowableTransferQueue
{
private static final Logger mLog = LoggerFactory.getLogger(OverflowableBufferStream.class);
private int mFlushCount = 0;
private Iterator mCurrentNativeBufferIterator;
private InterleavedComplexSamples mCurrentBuffer;
private int mCurrentBufferPointer = 0;
private FloatBuffer mFloatBuffer;
private LinkedList mBufferList = new LinkedList<>();
private int mBufferFetchLimit;
/**
* Stream for receiving reusable buffers from a producer thread and providing access to a consumer thread to read
* the sample data. Extends the overflowable transfer queue to define maximum and reset thresholds to ensure the
* queue doesn't grow too large.
*
* This stream is designed for consistent sample size requests each time the get() samples method is invoked, such
* as might be needed for an FFT processor. It uses an internal float buffer that must be resized whenever the
* get(sample size) argument changes, so consider using another option if you anticipate that your sample size
* argument during get() operations will vary.
*
* @param maxSize of the internal buffer queue - defines the overflow threshold
* @param resetThreshold of the internal buffer queue - defines when the overflow state will be cleared once the
* queue size has reduced below this threshold
* @param arraySize of the anticipated samples to fetch using the get(x,y) method.
*/
public OverflowableBufferStream(int maxSize, int resetThreshold, int arraySize)
{
super(maxSize, resetThreshold);
mFloatBuffer = FloatBuffer.allocate(arraySize);
mBufferFetchLimit = maxSize - resetThreshold;
}
/**
* Retrieves the specified number of float samples from the stream. When a non-zero overlap is specified, a
* quantity of samples from the previous buffer are preloaded in the buffer and new samples are added to make a
* full array count.
*
* @param sampleCount of float samples to pull from the stream
* @param overlap to reuse part of the previous buffer's samples in this fetch - must be less than sample count
* @return an array of float samples from the stream
* @throws IOException if the buffer queue is/becomes empty and the samples cannot be provided
* @throws IllegalArgumentException if the overlap argument is not less than the sample count argument
*/
public float[] get(int sampleCount, int overlap) throws IOException
{
if(overlap >= sampleCount)
{
throw new IllegalArgumentException("Overlap must be less than the requested sample count");
}
if(mCurrentBuffer == null)
{
//This will throw an IO exception if the queue is empty
getNextBuffer();
}
//Resize the float buffer if the request size changes
if(mFloatBuffer.capacity() != sampleCount)
{
mFloatBuffer = FloatBuffer.allocate(sampleCount);
}
while(mFloatBuffer.hasRemaining())
{
int available = mCurrentBuffer.samples().length - mCurrentBufferPointer;
if(available <= 0)
{
getNextBuffer();
}
else
{
//Fill the buffer to capacity
int toCopy = mFloatBuffer.remaining();
if(available < toCopy)
{
toCopy = available;
}
if(toCopy > 0)
{
mFloatBuffer.put(mCurrentBuffer.samples(), mCurrentBufferPointer, toCopy);
mCurrentBufferPointer += toCopy;
}
}
}
//If we get to here, the float buffer is full. Get the samples and then refill with overlap if non-zero
mFloatBuffer.rewind();
float[] samples = new float[sampleCount];
mFloatBuffer.get(samples);
mFloatBuffer.clear();
if(overlap > 0)
{
mFloatBuffer.put(samples, samples.length - overlap, overlap);
}
try
{
performFlush();
}
catch(IOException ioe)
{
//Do nothing if we get an IO exception ... the buffer is empty
}
//Reset flush count to zero, even if we ran out of buffers
mFlushCount = 0;
return samples;
}
/**
* Throws away incoming sample data until mFlushCount is zeroized or the queue is empty.
*
* @throws IOException if the queue is emptied while performing flush.
*/
private void performFlush() throws IOException
{
//Flush sample data as requested
while(mFlushCount > 0)
{
int available = mCurrentBuffer.samples().length - mCurrentBufferPointer;
if(available <= mFlushCount)
{
mFlushCount -= available;
getNextBuffer();
}
else
{
mCurrentBufferPointer += mFlushCount;
mFlushCount = 0;
}
}
}
/**
* Flushes the specified number of samples from the incoming stream.
* @param sampleCount to throw away
*/
public void flush(int sampleCount)
{
mFlushCount += sampleCount;
}
/**
* Fetches the next buffer from the queue.
*
* @throws IOException if the queue is currently empty
*/
private void getNextBuffer() throws IOException
{
if(mCurrentNativeBufferIterator == null)
{
if(mBufferList.isEmpty())
{
drainTo(mBufferList, mBufferFetchLimit);
}
if(!mBufferList.isEmpty())
{
T buffer = mBufferList.poll();
if(buffer != null)
{
mCurrentNativeBufferIterator = buffer.iteratorInterleaved();
}
}
}
if(mCurrentNativeBufferIterator != null && !mCurrentNativeBufferIterator.hasNext())
{
mCurrentNativeBufferIterator = null;
getNextBuffer();
return;
}
if(mCurrentNativeBufferIterator != null && mCurrentNativeBufferIterator.hasNext())
{
mCurrentBuffer = mCurrentNativeBufferIterator.next();
mCurrentBufferPointer = 0;
}
else
{
mCurrentBuffer = null;
throw new IOException("Buffer queue is empty");
}
}
/**
* Clears all elements from the queue and resets the internal counter to 0
*/
@Override
public void clear()
{
synchronized(mQueue)
{
mQueue.clear();
mCounter.set(0);
mOverflow.set(false);
}
}
}