mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Updates pull request from Roman Tsypuk to convert the build system from
ant to gradle. Removes linux and windows application launch scripts and produces an executable jar with all resources included.
This commit is contained in:
482
src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java
Normal file
482
src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java
Normal file
@@ -0,0 +1,482 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.spectrum;
|
||||
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Buffer;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.SampleType;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import io.github.dsheirer.spectrum.converter.DFTResultsConverter;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user