mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
483 lines
13 KiB
Java
483 lines
13 KiB
Java
/*******************************************************************************
|
|
* sdrtrunk
|
|
* Copyright (C) 2014-2017 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 <http://www.gnu.org/licenses/>
|
|
*
|
|
******************************************************************************/
|
|
package spectrum;
|
|
|
|
import dsp.filter.Window;
|
|
import dsp.filter.Window.WindowType;
|
|
import org.jtransforms.fft.FloatFFT_1D;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import properties.SystemProperties;
|
|
import sample.Buffer;
|
|
import sample.Listener;
|
|
import sample.SampleType;
|
|
import sample.complex.ComplexBuffer;
|
|
import source.tuner.frequency.FrequencyChangeEvent;
|
|
import source.tuner.frequency.IFrequencyChangeProcessor;
|
|
import spectrum.converter.DFTResultsConverter;
|
|
import util.ThreadPool;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.concurrent.ArrayBlockingQueue;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
/**
|
|
* Processes both complex samples or float samples and dispatches a float array
|
|
* of DFT results, using configurable fft size and output dispatch timelines.
|
|
*/
|
|
public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangeProcessor, IDFTWidthChangeProcessor
|
|
{
|
|
private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class);
|
|
|
|
private CopyOnWriteArrayList<DFTResultsConverter> mListeners = new CopyOnWriteArrayList<DFTResultsConverter>();
|
|
|
|
//Fixed size sample queue. We set the size at 12 based on the highest
|
|
//sample rate tuner (10 MHz) producing 153 frames per second and the lowest
|
|
//DFT processing frame rate of 12 frames per second. This means that the
|
|
//maximum size needed is 153 / 12 = 11 frames per DFT processing iteration
|
|
//so we set it at 12.
|
|
private BlockingQueue<ComplexBuffer> mQueue = new ArrayBlockingQueue<>(12);
|
|
|
|
private ScheduledFuture<?> mProcessorTaskHandle;
|
|
|
|
public static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
|
|
|
|
private DFTSize mDFTSize = DFTSize.FFT04096;
|
|
private DFTSize mNewDFTSize = DFTSize.FFT04096;
|
|
|
|
private double[] mWindow;
|
|
|
|
/* The Cosine and Hanning windows seem to offer the best spectral display
|
|
* with minimal bin leakage/smearing */
|
|
private WindowType mWindowType = Window.WindowType.HANNING;
|
|
|
|
private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize());
|
|
|
|
private int mFrameRate;
|
|
private int mSampleRate;
|
|
private int mFFTFloatsPerFrame;
|
|
private float mNewFloatsPerFrame;
|
|
private float mNewFloatResidual;
|
|
private float[] mPreviousFrame = new float[8192];
|
|
|
|
private float[] mCurrentBuffer;
|
|
private int mCurrentBufferPointer = 0;
|
|
|
|
private SampleType mSampleType;
|
|
|
|
private AtomicBoolean mRunning = new AtomicBoolean();
|
|
|
|
public DFTProcessor(SampleType sampleType)
|
|
{
|
|
setSampleType(sampleType);
|
|
mFrameRate = SystemProperties.getInstance().get(FRAME_RATE_PROPERTY, 20);
|
|
calculateConsumptionRate();
|
|
start();
|
|
}
|
|
|
|
public void dispose()
|
|
{
|
|
stop();
|
|
|
|
mListeners.clear();
|
|
mQueue.clear();
|
|
mWindow = null;
|
|
mCurrentBuffer = null;
|
|
}
|
|
|
|
public WindowType getWindowType()
|
|
{
|
|
return mWindowType;
|
|
}
|
|
|
|
public void setWindowType(WindowType windowType)
|
|
{
|
|
mWindowType = windowType;
|
|
|
|
if(mSampleType == SampleType.COMPLEX)
|
|
{
|
|
mWindow = Window.getWindow(mWindowType, mDFTSize.getSize() * 2);
|
|
}
|
|
else
|
|
{
|
|
mWindow = Window.getWindow(mWindowType, mDFTSize.getSize());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the processor mode to Float or Complex, depending on the sample
|
|
* types that will be delivered for processing
|
|
*/
|
|
public void setSampleType(SampleType type)
|
|
{
|
|
mSampleType = type;
|
|
setWindowType(mWindowType);
|
|
}
|
|
|
|
public SampleType getSampleType()
|
|
{
|
|
return mSampleType;
|
|
}
|
|
|
|
/**
|
|
* Queues an FFT size change request. The scheduled executor will apply
|
|
* the change when it runs.
|
|
*/
|
|
public void setDFTSize(DFTSize size)
|
|
{
|
|
mNewDFTSize = size;
|
|
}
|
|
|
|
public DFTSize getDFTSize()
|
|
{
|
|
return mDFTSize;
|
|
}
|
|
|
|
public int getFrameRate()
|
|
{
|
|
return mFrameRate;
|
|
}
|
|
|
|
public void setFrameRate(int framesPerSecond)
|
|
{
|
|
//TODO: make sure frame rate & sample rate sample requirement doesn't
|
|
//expect overlap greater than the previous frame length
|
|
|
|
if(framesPerSecond < 1 || framesPerSecond > 1000)
|
|
{
|
|
throw new IllegalArgumentException("DFTProcessor cannot run "
|
|
+ "more than 1000 times per second -- requested setting:"
|
|
+ framesPerSecond);
|
|
}
|
|
|
|
mFrameRate = framesPerSecond;
|
|
|
|
SystemProperties.getInstance().set(FRAME_RATE_PROPERTY, mFrameRate);
|
|
|
|
calculateConsumptionRate();
|
|
|
|
restart();
|
|
}
|
|
|
|
public void start()
|
|
{
|
|
//Schedule the DFT to run calculations at a fixed rate
|
|
int initialDelay = 0;
|
|
int period = (int) (1000 / mFrameRate);
|
|
|
|
mProcessorTaskHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new DFTCalculationTask(), initialDelay, period,
|
|
TimeUnit.MILLISECONDS);
|
|
}
|
|
|
|
public void stop()
|
|
{
|
|
//Cancel running DFT calculation task
|
|
if(mProcessorTaskHandle != null)
|
|
{
|
|
mProcessorTaskHandle.cancel(true);
|
|
}
|
|
}
|
|
|
|
public void restart()
|
|
{
|
|
stop();
|
|
start();
|
|
}
|
|
|
|
public int getCalculationsPerSecond()
|
|
{
|
|
return mFrameRate;
|
|
}
|
|
|
|
/**
|
|
* Places the sample into a transfer queue for future processing.
|
|
*/
|
|
@Override
|
|
public void receive(ComplexBuffer sampleBuffer)
|
|
{
|
|
mQueue.offer(sampleBuffer);
|
|
}
|
|
|
|
private void getNextBuffer()
|
|
{
|
|
mCurrentBuffer = null;
|
|
|
|
try
|
|
{
|
|
Buffer buffer = mQueue.take();
|
|
mCurrentBuffer = buffer.getSamples();
|
|
}
|
|
catch(InterruptedException e)
|
|
{
|
|
mCurrentBuffer = null;
|
|
}
|
|
|
|
mCurrentBufferPointer = 0;
|
|
}
|
|
|
|
private float[] getSamples()
|
|
{
|
|
int remaining = (int) mFFTFloatsPerFrame;
|
|
|
|
float[] currentFrame = new float[remaining];
|
|
|
|
int currentFramePointer = 0;
|
|
|
|
float integralFloatsToConsume = mNewFloatsPerFrame + mNewFloatResidual;
|
|
|
|
int newFloatsToConsumeThisFrame = (int) integralFloatsToConsume;
|
|
|
|
mNewFloatResidual = integralFloatsToConsume - newFloatsToConsumeThisFrame;
|
|
|
|
/* If the number of required floats for the fft is greater than the
|
|
* consumption rate per frame, we have to reach into the previous
|
|
* frame to makeup the difference. */
|
|
if(newFloatsToConsumeThisFrame < remaining)
|
|
{
|
|
int previousFloatsRequired = remaining - newFloatsToConsumeThisFrame;
|
|
|
|
System.arraycopy(mPreviousFrame,
|
|
mPreviousFrame.length - previousFloatsRequired,
|
|
currentFrame,
|
|
currentFramePointer,
|
|
previousFloatsRequired);
|
|
|
|
remaining -= previousFloatsRequired;
|
|
currentFramePointer += previousFloatsRequired;
|
|
}
|
|
|
|
/* Fill the rest of the buffer with new samples */
|
|
while(mRunning.get() && remaining > 0)
|
|
{
|
|
if(mCurrentBuffer == null ||
|
|
mCurrentBufferPointer >= mCurrentBuffer.length)
|
|
{
|
|
getNextBuffer();
|
|
}
|
|
|
|
/* If we don't have new samples to use, send the current frame with
|
|
* the remaining values as zero */
|
|
if(mCurrentBuffer == null)
|
|
{
|
|
remaining = 0;
|
|
}
|
|
else
|
|
{
|
|
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
|
|
|
|
while(remaining > 0 && samplesAvailable > 0)
|
|
{
|
|
currentFrame[currentFramePointer++] =
|
|
(float) mCurrentBuffer[mCurrentBufferPointer++];
|
|
|
|
samplesAvailable--;
|
|
remaining--;
|
|
newFloatsToConsumeThisFrame--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If the incoming float rate is greater than the fft consumption rate,
|
|
* then we have to purge some floats, otherwise, store the previous
|
|
* frame, because we have overlapping frames */
|
|
if(newFloatsToConsumeThisFrame > 0)
|
|
{
|
|
purge(newFloatsToConsumeThisFrame);
|
|
}
|
|
else
|
|
{
|
|
mPreviousFrame = Arrays.copyOf(currentFrame, currentFrame.length);
|
|
}
|
|
|
|
return currentFrame;
|
|
}
|
|
|
|
private void calculate()
|
|
{
|
|
float[] samples = getSamples();
|
|
|
|
Window.apply(mWindow, samples);
|
|
|
|
if(mSampleType == SampleType.REAL)
|
|
{
|
|
mFFT.realForward(samples);
|
|
}
|
|
else
|
|
{
|
|
mFFT.complexForward(samples);
|
|
}
|
|
|
|
dispatch(samples);
|
|
}
|
|
|
|
|
|
private void purge(int samplesToPurge)
|
|
{
|
|
if(samplesToPurge <= 0)
|
|
{
|
|
throw new IllegalArgumentException("DFTProcessor - cannot purge "
|
|
+ "negative sample amount");
|
|
}
|
|
|
|
while(mRunning.get() && samplesToPurge > 0)
|
|
{
|
|
if(mCurrentBuffer == null ||
|
|
mCurrentBufferPointer >= mCurrentBuffer.length)
|
|
{
|
|
getNextBuffer();
|
|
}
|
|
|
|
if(mCurrentBuffer != null)
|
|
{
|
|
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
|
|
|
|
if(samplesAvailable >= samplesToPurge)
|
|
{
|
|
mCurrentBufferPointer += samplesToPurge;
|
|
samplesToPurge = 0;
|
|
}
|
|
else
|
|
{
|
|
samplesToPurge -= samplesAvailable;
|
|
mCurrentBufferPointer = mCurrentBuffer.length;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
samplesToPurge = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes a calculated DFT results set, reformats the data, and sends it
|
|
* out to all registered listeners.
|
|
*/
|
|
private void dispatch(float[] results)
|
|
{
|
|
Iterator<DFTResultsConverter> it = mListeners.iterator();
|
|
|
|
while(it.hasNext())
|
|
{
|
|
it.next().receive(results);
|
|
}
|
|
}
|
|
|
|
public void addConverter(DFTResultsConverter listener)
|
|
{
|
|
mListeners.add(listener);
|
|
}
|
|
|
|
public void removeConverter(DFTResultsConverter listener)
|
|
{
|
|
mListeners.remove(listener);
|
|
}
|
|
|
|
private class DFTCalculationTask implements Runnable
|
|
{
|
|
@Override
|
|
public void run()
|
|
{
|
|
try
|
|
{
|
|
/* Only run if we're not currently running */
|
|
if(mRunning.compareAndSet(false, true))
|
|
{
|
|
checkFFTSize();
|
|
|
|
calculate();
|
|
|
|
mRunning.set(false);
|
|
}
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
mLog.error("error during dft processor calculation task", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for a queued FFT width change request and applies it. This
|
|
* method will only be accessed by the scheduled executor that gains
|
|
* access to run a calculate method, thus providing thread safety.
|
|
*/
|
|
private void checkFFTSize()
|
|
{
|
|
if(mNewDFTSize.getSize() != mDFTSize.getSize())
|
|
{
|
|
mDFTSize = mNewDFTSize;
|
|
|
|
calculateConsumptionRate();
|
|
|
|
setWindowType(mWindowType);
|
|
|
|
if(mSampleType == SampleType.COMPLEX)
|
|
{
|
|
mPreviousFrame = new float[mDFTSize.getSize() * 2];
|
|
}
|
|
else
|
|
{
|
|
mPreviousFrame = new float[mDFTSize.getSize()];
|
|
}
|
|
|
|
mFFT = new FloatFFT_1D(mDFTSize.getSize());
|
|
}
|
|
}
|
|
|
|
public void clearBuffer()
|
|
{
|
|
mQueue.clear();
|
|
}
|
|
|
|
@Override
|
|
public void frequencyChanged(FrequencyChangeEvent event)
|
|
{
|
|
switch(event.getEvent())
|
|
{
|
|
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
|
mSampleRate = event.getValue().intValue();
|
|
calculateConsumptionRate();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
private void calculateConsumptionRate()
|
|
{
|
|
mNewFloatResidual = 0.0f;
|
|
|
|
mNewFloatsPerFrame = ((float) mSampleRate / (float) mFrameRate) *
|
|
(mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f);
|
|
|
|
mFFTFloatsPerFrame = (mSampleType == SampleType.COMPLEX ?
|
|
mDFTSize.getSize() * 2 :
|
|
mDFTSize.getSize());
|
|
}
|
|
}
|