mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Updates the Oscillator and LowPhaseNoiseOscillator classes to share a
common interface and abstract class.
This commit is contained in:
2
.idea/dictionaries/denny.xml
generated
2
.idea/dictionaries/denny.xml
generated
@@ -5,6 +5,8 @@
|
||||
<w>broadcastify</w>
|
||||
<w>esnid</w>
|
||||
<w>fleetsync</w>
|
||||
<w>heterdyned</w>
|
||||
<w>heterodynes</w>
|
||||
<w>icecast</w>
|
||||
<w>ident</w>
|
||||
<w>nutall</w>
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
public class AudioAdapter
|
||||
{
|
||||
private static int mGain = 5;
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private IOscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionFilter;
|
||||
private FloatFIRFilter mAudioHighPassFilter;
|
||||
private AudioType mAudioType = AudioType.NORMAL;
|
||||
@@ -88,7 +89,7 @@ public class AudioAdapter
|
||||
default:
|
||||
retVal = (float)( mPostInversionFilter.get(
|
||||
mAudioHighPassFilter.get( sample ) *
|
||||
mSineWaveGenerator.getFloat() ) );
|
||||
mSineWaveGenerator.inphase() ) );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
break;
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
|
||||
import io.github.dsheirer.audio.InversionFrequency;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
@@ -29,7 +30,7 @@ import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
*/
|
||||
public class AudioInverter implements RealSampleListener
|
||||
{
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private IOscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionLowPassFilter;
|
||||
private RealSampleListener mListener;
|
||||
|
||||
@@ -61,7 +62,7 @@ public class AudioInverter implements RealSampleListener
|
||||
if( mPostInversionLowPassFilter != null )
|
||||
{
|
||||
mPostInversionLowPassFilter.receive(
|
||||
sample * mSineWaveGenerator.getFloat() );
|
||||
sample * mSineWaveGenerator.inphase() );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.buffer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -8,87 +23,86 @@ import java.util.List;
|
||||
|
||||
public class CircularBuffer<T> implements Listener<T>
|
||||
{
|
||||
private ArrayList<T> mElements;
|
||||
private Listener<T> mListener;
|
||||
private int mPointer;
|
||||
private int mSize;
|
||||
private List<T> mElements;
|
||||
private Listener<T> mListener;
|
||||
private int mPointer;
|
||||
private int mSize;
|
||||
|
||||
|
||||
/**
|
||||
* Circular Buffer - stores elements in an array list. Once the buffer is
|
||||
* full, new samples replace older samples and older samples are broadcast
|
||||
* to an optional listener.
|
||||
*
|
||||
* @param size - size of buffer
|
||||
*/
|
||||
public CircularBuffer( int size )
|
||||
{
|
||||
mSize = size;
|
||||
mElements = new ArrayList<T>();
|
||||
}
|
||||
/**
|
||||
* Circular Buffer - stores elements in an array list. Once the buffer is
|
||||
* full, new samples replace older samples and older samples are broadcast
|
||||
* to an optional listener.
|
||||
*
|
||||
* @param size - size of buffer
|
||||
*/
|
||||
public CircularBuffer(int size)
|
||||
{
|
||||
mSize = size;
|
||||
mElements = new ArrayList<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element stored at index. Note: the index argument refers to
|
||||
* the internal storage index and does not indicate a temporal relationship
|
||||
* among elements. For example, if you get indexes 1, 2, and 3, you may be
|
||||
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
|
||||
* is currently pointing to and will insert the next element at index 4.
|
||||
*
|
||||
* @param index - element index
|
||||
*
|
||||
* @return element or null
|
||||
*/
|
||||
public T get( int index )
|
||||
{
|
||||
if( index < mElements.size() )
|
||||
{
|
||||
return mElements.get( index );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mSize;
|
||||
}
|
||||
|
||||
public List<T> getElements()
|
||||
{
|
||||
return Collections.unmodifiableList( mElements );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( T element )
|
||||
{
|
||||
T previous = null;
|
||||
|
||||
if( mElements.size() > mPointer )
|
||||
{
|
||||
previous = mElements.get( mPointer );
|
||||
|
||||
mElements.set( mPointer, element );
|
||||
}
|
||||
else
|
||||
{
|
||||
mElements.add( element );
|
||||
}
|
||||
|
||||
mPointer++;
|
||||
|
||||
if( mPointer >= mSize )
|
||||
{
|
||||
mPointer = 0;
|
||||
}
|
||||
|
||||
if( previous != null && mListener != null )
|
||||
{
|
||||
mListener.receive( previous );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the element stored at index. Note: the index argument refers to
|
||||
* the internal storage index and does not indicate a temporal relationship
|
||||
* among elements. For example, if you get indexes 1, 2, and 3, you may be
|
||||
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
|
||||
* is currently pointing to and will insert the next element at index 4.
|
||||
*
|
||||
* @param index - element index
|
||||
* @return element or null
|
||||
*/
|
||||
public T get(int index)
|
||||
{
|
||||
if(index < mElements.size())
|
||||
{
|
||||
return mElements.get(index);
|
||||
}
|
||||
|
||||
public void setListener( Listener<T> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mSize;
|
||||
}
|
||||
|
||||
public List<T> getElements()
|
||||
{
|
||||
return Collections.unmodifiableList(mElements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(T element)
|
||||
{
|
||||
T previous = null;
|
||||
|
||||
if(mElements.size() > mPointer)
|
||||
{
|
||||
previous = mElements.get(mPointer);
|
||||
|
||||
mElements.set(mPointer, element);
|
||||
}
|
||||
else
|
||||
{
|
||||
mElements.add(element);
|
||||
}
|
||||
|
||||
mPointer++;
|
||||
|
||||
if(mPointer >= mSize)
|
||||
{
|
||||
mPointer = 0;
|
||||
}
|
||||
|
||||
if(previous != null && mListener != null)
|
||||
{
|
||||
mListener.receive(previous);
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(Listener<T> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ package io.github.dsheirer.buffer;
|
||||
*
|
||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
||||
*/
|
||||
public class FloatCircularBuffer
|
||||
public class RealCircularBuffer
|
||||
{
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
public FloatCircularBuffer(int size)
|
||||
public RealCircularBuffer(int size)
|
||||
{
|
||||
mBuffer = new float[size];
|
||||
}
|
||||
@@ -727,6 +727,42 @@ public class FilterFactory
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param windowType to use
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
*/
|
||||
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
{
|
||||
return getSincFilter(channelBandwidth * channels * 2.0, channelBandwidth,
|
||||
channels, tapsPerChannel, windowType,logResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and
|
||||
* an M*channel count output.
|
||||
*
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param windowType to use
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
*/
|
||||
public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
{
|
||||
return getSincFilter(channelBandwidth * channels, channelBandwidth, channels,
|
||||
tapsPerChannel, windowType,logResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a windowed-sync (Nyquist) M/2 prototype FIR filter for use with a Polyphase Channelizer/Synthesizer.
|
||||
* The filter is designed for x2 oversampling of each channel and the filter cutoff frequency is incrementally
|
||||
@@ -752,14 +788,13 @@ public class FilterFactory
|
||||
*
|
||||
* @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB
|
||||
*/
|
||||
public static float[] getSincChannelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
public static float[] getSincFilter(double sampleRate, double channelBandwidth, int channels,
|
||||
int tapsPerChannel, Window.WindowType windowType, boolean logResults) throws FilterDesignException
|
||||
{
|
||||
int currentTapsPerChannel = tapsPerChannel;
|
||||
int sampleRate = (int)(channelBandwidth * (double)channels * 2.0);
|
||||
int filterLength = (channels * currentTapsPerChannel) - 1;
|
||||
|
||||
double cutoffFrequency = channelBandwidth / (double)sampleRate;
|
||||
double cutoffFrequency = channelBandwidth / sampleRate;
|
||||
double bandEdge = cutoffFrequency;
|
||||
double increment = cutoffFrequency * 0.1;
|
||||
|
||||
@@ -768,7 +803,7 @@ public class FilterFactory
|
||||
double response = FilterFactory.evaluate(taps, bandEdge);
|
||||
|
||||
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
|
||||
double incrementThreshold = 1.0 / (double)sampleRate;
|
||||
double incrementThreshold = 1.0 / sampleRate;
|
||||
|
||||
//Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective
|
||||
while(increment > incrementThreshold)
|
||||
@@ -819,7 +854,7 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
filterLength = channels * currentTapsPerChannel - 1;
|
||||
cutoffFrequency = ((double)channelBandwidth / (double)sampleRate);
|
||||
cutoffFrequency = channelBandwidth / sampleRate;
|
||||
increment = cutoffFrequency * 0.1;
|
||||
}
|
||||
|
||||
|
||||
@@ -396,7 +396,8 @@ public class ChannelCalculator
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Illegal channel index");
|
||||
throw new IllegalArgumentException("Illegal channel index [" + index + "] current max channels[" +
|
||||
getChannelCount() + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
@@ -516,18 +517,19 @@ public class ChannelCalculator
|
||||
throw new IllegalArgumentException("Indexes cannot be empty");
|
||||
}
|
||||
|
||||
int middleIndex = indexes.get(indexes.size() / 2);
|
||||
int centerIndex = (indexes.size() - 1) / 2;
|
||||
|
||||
if(indexes.size() % 2 == 0)
|
||||
int index = indexes.get(centerIndex);
|
||||
|
||||
if(index == getWrapAroundIndex())
|
||||
{
|
||||
//Even number of indexes
|
||||
return (long)getIndexMinimumFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
return (long)getIndexCenterFrequency(indexes.get(centerIndex), IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Odd number of indexes
|
||||
return (long)getIndexCenterFrequency(middleIndex, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
return (long)getIndexCenterFrequency(indexes.get(centerIndex), IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,7 +201,7 @@ public class ChannelizerViewer extends JFrame
|
||||
|
||||
long baseFrequency = mBaseFrequency + (CHANNEL_BANDWIDTH / 2);
|
||||
|
||||
for(int x = 0; x < mChannelCount - 1; x++)
|
||||
for(int x = 0; x < mChannelCount; x++)
|
||||
{
|
||||
long frequency = baseFrequency + (x * CHANNEL_BANDWIDTH);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
{
|
||||
super(sampleRate, (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH));
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(),
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
tapsPerFilter, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
double oversampledChannelSampleRate = getChannelSampleRate() * 2.0;
|
||||
@@ -107,7 +107,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
{
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincChannelizer(getChannelSampleRate(), getChannelCount(),
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
15, WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
init(filterTaps);
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
|
||||
@@ -32,7 +34,9 @@ import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
@@ -54,6 +58,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
|
||||
private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
|
||||
private ScheduledBufferProcessor mBufferProcessor;
|
||||
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
|
||||
@@ -166,7 +171,15 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
case 1:
|
||||
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes);
|
||||
case 2:
|
||||
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes);
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(2);
|
||||
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error designing 2 channel synthesis filter for output processor");
|
||||
}
|
||||
default:
|
||||
//TODO: create output processor for greater than 2 input channels
|
||||
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
|
||||
@@ -241,6 +254,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
//Update channel calculator immediately so that channels can be allocated
|
||||
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
|
||||
|
||||
mLog.debug("Tuner frequency change: " + sourceEvent.getValue().longValue());
|
||||
//Defer channelizer configuration changes to be handled on the buffer processor thread
|
||||
mBufferSourceEventMonitor.receive(sourceEvent);
|
||||
break;
|
||||
@@ -294,6 +308,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
mLog.error("Could not create filter for polyphase channelizer for sample rate [" + sampleRate + "]", fde);
|
||||
}
|
||||
|
||||
//Clear the previous channel synthesis filters so they can be recreated for the new channel sample rate
|
||||
mOutputProcessorFilters.clear();
|
||||
|
||||
//Broadcast the new channel sample rate
|
||||
double updatedChannelSampleRate = mChannelCalculator.getChannelSampleRate();
|
||||
updateOutputProcessors(SourceEvent.sampleRateChange(updatedChannelSampleRate));
|
||||
@@ -322,6 +339,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
try
|
||||
{
|
||||
mLog.debug("**Sending source event to channel: " + sourceEvent.toString());
|
||||
channelSource.process(sourceEvent);
|
||||
}
|
||||
catch(SourceException se)
|
||||
@@ -352,11 +370,26 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
//If the indexes size is the same then update the current processor, otherwise create a new one
|
||||
if(channelSource.getPolyphaseChannelOutputProcessor().getInputChannelCount() == indexes.size())
|
||||
{
|
||||
mLog.debug("Updating existing output processor - indexes:" + indexes + " freq:" + centerFrequency);
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
|
||||
channelSource.setFrequency(centerFrequency);
|
||||
|
||||
if(indexes.size() > 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(indexes.size());
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error creating an updated synthesis filter for the channel output processor");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Setting new output processor - indexes:" + indexes + " freq:" + centerFrequency);
|
||||
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
|
||||
}
|
||||
}
|
||||
@@ -409,6 +442,29 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
mSourceEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates (or reuses) an output processor filter for the specified number of channels. Each
|
||||
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
|
||||
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
|
||||
* @param channels count
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
|
||||
*/
|
||||
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
|
||||
{
|
||||
float[] taps = mOutputProcessorFilters.get(channels);
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels,
|
||||
POLYPHASE_FILTER_TAPS_PER_CHANNEL, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
mOutputProcessorFilters.put(channels, taps);
|
||||
}
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
|
||||
*/
|
||||
@@ -482,7 +538,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
switch(queuedSourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
updateOutputProcessors(queuedSourceEvent);
|
||||
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
|
||||
updateOutputProcessors(null);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
updateChannelizerSampleRate(queuedSourceEvent.getValue().intValue());
|
||||
|
||||
@@ -186,6 +186,7 @@ public class PolyphaseChannelSource extends TunerChannelSource
|
||||
@Override
|
||||
protected void setFrequency(long frequency)
|
||||
{
|
||||
mLog.debug("Center Frequency set to: " + frequency + " for channel " + getTunerChannel().toString());
|
||||
mCenterFrequency = frequency;
|
||||
|
||||
//Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.SampleType;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel;
|
||||
import io.github.dsheirer.spectrum.DFTProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SynthesizerViewer extends JFrame
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SynthesizerViewer.class);
|
||||
|
||||
private static final int CHANNEL_BANDWIDTH = 12500;
|
||||
private static final int CHANNEL_SAMPLE_RATE = 25000;
|
||||
private static final int CHANNEL_FFT_FRAME_RATE = 20; //frames per second
|
||||
private static final int DATA_GENERATOR_FRAME_RATE = 50; //frames per second
|
||||
|
||||
private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel());
|
||||
private JPanel mPrimaryPanel;
|
||||
private PrimarySpectrumPanel mPrimarySpectrumPanel;
|
||||
private ChannelPanel mChannel1Panel;
|
||||
private ChannelPanel mChannel2Panel;
|
||||
private ChannelControlPanel mChannel1ControlPanel;
|
||||
private ChannelControlPanel mChannel2ControlPanel;
|
||||
private DFTSize mMainPanelDFTSize = DFTSize.FFT32768;
|
||||
private DFTSize mChannelPanelDFTSize = DFTSize.FFT08192;
|
||||
|
||||
/**
|
||||
* GUI Test utility for researching polyphase synthesizers.
|
||||
*/
|
||||
public SynthesizerViewer()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new DataGenerationManager(), 0, 1000 / DATA_GENERATOR_FRAME_RATE, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setTitle("Polyphase Synthesizer Viewer");
|
||||
setSize(500, 400);
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
setLocationRelativeTo(null);
|
||||
add(getPrimaryPanel());
|
||||
}
|
||||
|
||||
private JPanel getPrimaryPanel()
|
||||
{
|
||||
if(mPrimaryPanel == null)
|
||||
{
|
||||
mPrimaryPanel = new JPanel();
|
||||
mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill]", "[grow,fill][grow,fill]"));
|
||||
mPrimaryPanel.add(getSpectrumPanel(), "span");
|
||||
mPrimaryPanel.add(getChannel1Panel());
|
||||
mPrimaryPanel.add(getChannel2Panel());
|
||||
}
|
||||
|
||||
return mPrimaryPanel;
|
||||
}
|
||||
|
||||
private PrimarySpectrumPanel getSpectrumPanel()
|
||||
{
|
||||
if(mPrimarySpectrumPanel == null)
|
||||
{
|
||||
mPrimarySpectrumPanel = new PrimarySpectrumPanel(mSettingsManager);
|
||||
mPrimarySpectrumPanel.setPreferredSize(new Dimension(500, 200));
|
||||
mPrimarySpectrumPanel.setDFTSize(mMainPanelDFTSize);
|
||||
}
|
||||
|
||||
return mPrimarySpectrumPanel;
|
||||
}
|
||||
|
||||
private ChannelPanel getChannel1Panel()
|
||||
{
|
||||
if(mChannel1Panel == null)
|
||||
{
|
||||
mChannel1Panel = new ChannelPanel(mSettingsManager, getChannel1ControlPanel());
|
||||
mChannel1Panel.setPreferredSize(new Dimension(250, 200));
|
||||
mChannel1Panel.setDFTSize(mChannelPanelDFTSize);
|
||||
}
|
||||
|
||||
return mChannel1Panel;
|
||||
}
|
||||
|
||||
private ChannelPanel getChannel2Panel()
|
||||
{
|
||||
if(mChannel2Panel == null)
|
||||
{
|
||||
mChannel2Panel = new ChannelPanel(mSettingsManager, getChannel2ControlPanel());
|
||||
mChannel2Panel.setPreferredSize(new Dimension(250, 200));
|
||||
mChannel2Panel.setDFTSize(mChannelPanelDFTSize);
|
||||
}
|
||||
|
||||
return mChannel2Panel;
|
||||
}
|
||||
|
||||
public class PrimarySpectrumPanel extends JPanel implements Listener<ComplexBuffer>
|
||||
{
|
||||
private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
|
||||
private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
|
||||
private SpectrumPanel mSpectrumPanel;
|
||||
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel.setSampleSize(12);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
mDFTProcessor.process(SourceEvent.sampleRateChange(CHANNEL_SAMPLE_RATE));
|
||||
mDFTProcessor.setFrameRate(CHANNEL_FFT_FRAME_RATE);
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
}
|
||||
|
||||
public void setDFTSize(DFTSize dftSize)
|
||||
{
|
||||
mDFTProcessor.setDFTSize(dftSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelPanel extends JPanel implements Listener<ComplexBuffer>
|
||||
{
|
||||
private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX);
|
||||
private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter();
|
||||
private SpectrumPanel mSpectrumPanel;
|
||||
|
||||
public ChannelPanel(SettingsManager settingsManager, ChannelControlPanel channelControlPanel)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel.setSampleSize(12);
|
||||
add(mSpectrumPanel, "wrap");
|
||||
add(channelControlPanel);
|
||||
|
||||
mDFTProcessor.addConverter(mComplexDecibelConverter);
|
||||
mDFTProcessor.process(SourceEvent.sampleRateChange(CHANNEL_SAMPLE_RATE));
|
||||
mDFTProcessor.setFrameRate(CHANNEL_FFT_FRAME_RATE);
|
||||
|
||||
mComplexDecibelConverter.addListener(mSpectrumPanel);
|
||||
}
|
||||
|
||||
public void setDFTSize(DFTSize dftSize)
|
||||
{
|
||||
mDFTProcessor.setDFTSize(dftSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
mDFTProcessor.receive(complexBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelControlPanel getChannel1ControlPanel()
|
||||
{
|
||||
if(mChannel1ControlPanel == null)
|
||||
{
|
||||
mChannel1ControlPanel = new ChannelControlPanel();
|
||||
}
|
||||
|
||||
return mChannel1ControlPanel;
|
||||
}
|
||||
|
||||
private ChannelControlPanel getChannel2ControlPanel()
|
||||
{
|
||||
if(mChannel2ControlPanel == null)
|
||||
{
|
||||
mChannel2ControlPanel = new ChannelControlPanel();
|
||||
}
|
||||
|
||||
return mChannel2ControlPanel;
|
||||
}
|
||||
|
||||
public class ChannelControlPanel extends JPanel
|
||||
{
|
||||
private static final int SAMPLE_RATE = 25000;
|
||||
private static final int MIN_FREQUENCY = -6250;
|
||||
private static final int MAX_FREQUENCY = 6250;
|
||||
private static final int DEFAULT_FREQUENCY = 50;
|
||||
|
||||
private IOscillator mOscillator = new LowPhaseNoiseOscillator(6000, CHANNEL_SAMPLE_RATE);
|
||||
|
||||
public ChannelControlPanel()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
add(new JLabel("Tone:"), "align right");
|
||||
|
||||
SpinnerNumberModel model = new SpinnerNumberModel(DEFAULT_FREQUENCY, MIN_FREQUENCY, MAX_FREQUENCY, 100);
|
||||
model.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
long toneFrequency = model.getNumber().longValue();
|
||||
mOscillator.setFrequency(toneFrequency);
|
||||
}
|
||||
});
|
||||
|
||||
JSpinner spinner = new JSpinner(model);
|
||||
add(spinner);
|
||||
add(new JLabel("Hz"));
|
||||
}
|
||||
|
||||
public IOscillator getOscillator()
|
||||
{
|
||||
return mOscillator;
|
||||
}
|
||||
}
|
||||
|
||||
public class DataGenerationManager implements Runnable
|
||||
{
|
||||
private TwoChannelSynthesizerM2 mSynthesizer;
|
||||
private int mSamplesPerCycle = 25000 / DATA_GENERATOR_FRAME_RATE;
|
||||
|
||||
public DataGenerationManager()
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19,
|
||||
Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
mSynthesizer = new TwoChannelSynthesizerM2(taps);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
float[] channel1 = getChannel1ControlPanel().getOscillator().generateComplex(mSamplesPerCycle);
|
||||
float[] channel2 = getChannel2ControlPanel().getOscillator().generateComplex(mSamplesPerCycle);
|
||||
|
||||
float[] synthesized = mSynthesizer.process(channel1, channel2);
|
||||
|
||||
getChannel1Panel().receive(new ComplexBuffer(channel1));
|
||||
getChannel2Panel().receive(new ComplexBuffer(channel2));
|
||||
getSpectrumPanel().receive(new ComplexBuffer(synthesized));
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
final SynthesizerViewer frame = new SynthesizerViewer();
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
frame.setVisible(true);
|
||||
frame.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.buffer.RealCircularBuffer;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class TwoChannelSynthesizerM2
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class);
|
||||
|
||||
private ComplexFIRFilter mFilter1;
|
||||
private ComplexFIRFilter mFilter2;
|
||||
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D(2);
|
||||
private boolean mFlag = false;
|
||||
|
||||
/**
|
||||
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using
|
||||
* perfect reconstruction filters (-6db at band edge). Output sample rate is the same as the
|
||||
* input sample rate of one of the channels.
|
||||
*/
|
||||
public TwoChannelSynthesizerM2(float[] filter)
|
||||
{
|
||||
init(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partitions the filter into 2 polyphase partitions and uses only the first filter. The second
|
||||
* filter partition and all corresponding samples represent the oversampling excess that will be
|
||||
* thrown away.
|
||||
*
|
||||
* We create two instances of filter 1 to simplify buffer management where we have to swap the data
|
||||
* buffer at each sample to correct for phase shift introduced by buffer swapping when down sampling
|
||||
* from the M2 sample rates of each channel.
|
||||
*
|
||||
* @param filter to partition into 2 polyphase filters.
|
||||
*/
|
||||
private void init(float[] filter)
|
||||
{
|
||||
int length = (filter.length / 2) + (filter.length % 2);
|
||||
|
||||
float[] filter1 = new float[length];
|
||||
float[] filter2 = new float[length];
|
||||
|
||||
for(int x = 0; x < filter.length; x += 2)
|
||||
{
|
||||
//Load the filter in reverse to facilitate convolution
|
||||
int index = length - (x / 2) - 1;
|
||||
|
||||
filter1[index] = filter[x];
|
||||
|
||||
if(x + 1 < filter.length)
|
||||
{
|
||||
filter2[index] = filter[x + 1];
|
||||
}
|
||||
}
|
||||
|
||||
mFilter1 = new ComplexFIRFilter(filter1, 1.0f);
|
||||
mFilter2 = new ComplexFIRFilter(filter2, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesizes a new channel from the channel 1 and channel 2 sample arrays that are arranged as
|
||||
* i0, q0, i1, q1 ... iN-1, qN-1
|
||||
*
|
||||
* @param channel1 input channel
|
||||
* @param channel2 input channel
|
||||
* @return synthesized channel results of the same length as both channel 1/2 input arrays.
|
||||
*/
|
||||
public float[] process(float[] channel1, float[] channel2)
|
||||
{
|
||||
if(channel1.length != channel2.length)
|
||||
{
|
||||
throw new IllegalArgumentException("Channel 1 and 2 array length must be equal");
|
||||
}
|
||||
|
||||
float[] output = new float[channel1.length];
|
||||
|
||||
float[] buffer = new float[4];
|
||||
|
||||
for(int x = 0; x < channel1.length; x += 2)
|
||||
{
|
||||
buffer[0] = channel1[x]; //i
|
||||
buffer[1] = channel1[x + 1]; //q
|
||||
buffer[2] = channel2[x]; //i
|
||||
buffer[3] = channel2[x + 1]; //q
|
||||
|
||||
mFFT.complexInverse(buffer, true);
|
||||
|
||||
if(mFlag)
|
||||
{
|
||||
output[x] = mFilter1.filterInphase(buffer[0]) + mFilter2.filterInphase(buffer[2]);
|
||||
output[x + 1] = mFilter1.filterQuadrature(buffer[1]) + mFilter2.filterQuadrature(buffer[3]);
|
||||
}
|
||||
else
|
||||
{
|
||||
output[x] = mFilter1.filterInphase(buffer[2]) + mFilter2.filterInphase(buffer[0]);
|
||||
output[x + 1] = mFilter1.filterQuadrature(buffer[3]) + mFilter2.filterQuadrature(buffer[1]);
|
||||
}
|
||||
|
||||
mFlag = !mFlag;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static float filter(float[] taps, RealCircularBuffer dataBlock)
|
||||
{
|
||||
float accumulator = 0;
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
accumulator += (taps[x] * dataBlock.get(x));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting");
|
||||
|
||||
int sampleCount = 10;
|
||||
|
||||
try
|
||||
{
|
||||
float[] taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
|
||||
TwoChannelSynthesizerM2 synthesizer = new TwoChannelSynthesizerM2(taps);
|
||||
|
||||
IOscillator oscillator = new LowPhaseNoiseOscillator(3000.0, 25000.0);
|
||||
|
||||
float[] channel1 = oscillator.generateComplex(sampleCount);
|
||||
float[] channel2 = new float[sampleCount * 2];
|
||||
|
||||
float[] synthesized = synthesizer.process(channel1, channel2);
|
||||
|
||||
mLog.debug("1:" + Arrays.toString(channel1));
|
||||
mLog.debug("X:" + Arrays.toString(synthesized));
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
|
||||
mLog.debug("Finished!!");
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.sample.complex.TimestampedBufferAssembler;
|
||||
@@ -40,7 +41,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
|
||||
private int mInputChannelCount;
|
||||
//TODO: swap this out and use the LowPhaseNoiseOscillator
|
||||
private Oscillator mFrequencyCorrectionMixer;
|
||||
private IOscillator mFrequencyCorrectionMixer;
|
||||
private boolean mFrequencyCorrectionEnabled;
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
/**
|
||||
* Oscillator/mixer to use for frequency correction against incominb sample stream
|
||||
*/
|
||||
protected Oscillator getFrequencyCorrectionMixer()
|
||||
protected IOscillator getFrequencyCorrectionMixer()
|
||||
{
|
||||
return mFrequencyCorrectionMixer;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ public interface IPolyphaseChannelOutputProcessor
|
||||
*/
|
||||
void setPolyphaseChannelIndices(List<Integer> indexes);
|
||||
|
||||
/**
|
||||
* Updates the synthesis filter taps for this output processor
|
||||
* @param filter
|
||||
*/
|
||||
void setSynthesisFilter(float[] filter);
|
||||
|
||||
/**
|
||||
* Sets the listener to receive buffer overflow notifications
|
||||
* @param source to receive overflow notifications
|
||||
|
||||
@@ -44,6 +44,12 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSynthesisFilter(float[] filter)
|
||||
{
|
||||
throw new IllegalArgumentException("The one channel output processor does not support filter updates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this processor to extract a single, specified channel index.
|
||||
* @param indexes containing a single channel index value.
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelResultsBuffer;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||
import io.github.dsheirer.sample.complex.TimestampedBufferAssembler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -27,6 +27,10 @@ import java.util.List;
|
||||
|
||||
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private ComplexFIRFilter mLowPassFilter =
|
||||
new ComplexFIRFilter(Filters.HALF_BAND_FILTER_47T.getCoefficients(), 1.0f);
|
||||
private TwoChannelSynthesizerM2 mSynthesizer;
|
||||
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
|
||||
|
||||
private int mChannelOffset1;
|
||||
@@ -39,12 +43,31 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing two channel indices.
|
||||
*/
|
||||
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes)
|
||||
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter)
|
||||
{
|
||||
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
|
||||
//after synthesizing both input channels
|
||||
super(2, sampleRate * 2);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
setSynthesisFilter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
|
||||
* @param frequencyOffset in hertz
|
||||
*/
|
||||
@Override
|
||||
public void setFrequencyOffset(long frequencyOffset)
|
||||
{
|
||||
// super.setFrequencyOffset(frequencyOffset);
|
||||
mLog.debug("**Ignoring** Setting frequency offset [" + frequencyOffset + "] for channel offsets [" + mChannelOffset1 + "," + mChannelOffset2 + "]");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setSynthesisFilter(float[] filter)
|
||||
{
|
||||
mSynthesizer = new TwoChannelSynthesizerM2(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +101,22 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
public void process(List<PolyphaseChannelResultsBuffer> channelResultsBuffers,
|
||||
TimestampedBufferAssembler timestampedBufferAssembler)
|
||||
{
|
||||
mLog.debug("I got channel results - doing nothing !!");
|
||||
for(PolyphaseChannelResultsBuffer buffer: channelResultsBuffers)
|
||||
{
|
||||
float[] channel1 = buffer.getChannel(mChannelOffset1);
|
||||
float[] channel2 = buffer.getChannel(mChannelOffset2);
|
||||
|
||||
//Join the two channels using the synthesizer
|
||||
float[] synthesized = mSynthesizer.process(channel1, channel2);
|
||||
|
||||
//Apply frequency correction to center the signal of interest within the synthesized channel
|
||||
getFrequencyCorrectionMixer().mixComplex(synthesized);
|
||||
|
||||
//Apply low-pass filtering to attenuate neighboring channel(s)
|
||||
// synthesized = mLowPassFilter.filter(synthesized);
|
||||
|
||||
//Send it on its merry way ...
|
||||
timestampedBufferAssembler.receive(synthesized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.gui.control.CurveFittedAreaChart;
|
||||
@@ -12,155 +27,163 @@ import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class FilterView extends BorderPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( FilterView.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FilterView.class);
|
||||
|
||||
private CurveFittedAreaChart mImpulseChart;
|
||||
private NumberAxis mImpulseXAxis;
|
||||
private NumberAxis mImpulseYAxis;
|
||||
private CurveFittedAreaChart mDFTChart;
|
||||
private NumberAxis mDFTXAxis;
|
||||
private NumberAxis mDFTYAxis;
|
||||
private static final int FFT_SIZE = 4096;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE );
|
||||
|
||||
public FilterView( float[] taps )
|
||||
{
|
||||
init();
|
||||
private CurveFittedAreaChart mImpulseChart;
|
||||
private NumberAxis mImpulseXAxis;
|
||||
private NumberAxis mImpulseYAxis;
|
||||
private CurveFittedAreaChart mDFTChart;
|
||||
private NumberAxis mDFTXAxis;
|
||||
private NumberAxis mDFTYAxis;
|
||||
private static final int FFT_SIZE = 4096;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE);
|
||||
|
||||
if( taps != null )
|
||||
{
|
||||
setImpulseTaps( taps );
|
||||
public FilterView(float[] taps)
|
||||
{
|
||||
init();
|
||||
|
||||
float[] dft = new float[ FFT_SIZE ];
|
||||
System.arraycopy( taps, 0, dft, 0, taps.length );
|
||||
|
||||
mFFT.realForward( dft );
|
||||
|
||||
setDFTTaps( convertDFTToDecibel( dft, taps.length ) );
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
|
||||
mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 );
|
||||
mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 );
|
||||
|
||||
mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis );
|
||||
mImpulseChart.setLegendVisible( false );
|
||||
mImpulseChart.setHorizontalGridLinesVisible( false );
|
||||
mImpulseChart.setVerticalGridLinesVisible(false);
|
||||
if(taps != null)
|
||||
{
|
||||
setImpulseTaps(taps);
|
||||
|
||||
float[] dft = new float[FFT_SIZE];
|
||||
System.arraycopy(taps, 0, dft, 0, taps.length);
|
||||
|
||||
mFFT.realForward(dft);
|
||||
|
||||
setDFTTaps(convertDFTToDecibel(dft, taps.length));
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
|
||||
mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0);
|
||||
mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1);
|
||||
|
||||
mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis);
|
||||
mImpulseChart.setLegendVisible(false);
|
||||
mImpulseChart.setHorizontalGridLinesVisible(false);
|
||||
mImpulseChart.setVerticalGridLinesVisible(false);
|
||||
mImpulseChart.setAlternativeColumnFillVisible(false);
|
||||
mImpulseChart.setAlternativeRowFillVisible(false);
|
||||
|
||||
String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm();
|
||||
mImpulseChart.getStylesheets().add( css );
|
||||
|
||||
hbox.getChildren().add( mImpulseChart );
|
||||
HBox.setHgrow( mImpulseChart, Priority.ALWAYS );
|
||||
|
||||
mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 );
|
||||
mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 );
|
||||
|
||||
mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis );
|
||||
mDFTChart.setLegendVisible( false );
|
||||
mDFTChart.setHorizontalGridLinesVisible( false );
|
||||
mDFTChart.setVerticalGridLinesVisible(false);
|
||||
hbox.getChildren().add(mImpulseChart);
|
||||
HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
|
||||
|
||||
mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8);
|
||||
mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0);
|
||||
|
||||
mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis);
|
||||
mDFTChart.setLegendVisible(false);
|
||||
mDFTChart.setHorizontalGridLinesVisible(false);
|
||||
mDFTChart.setVerticalGridLinesVisible(false);
|
||||
mDFTChart.setAlternativeColumnFillVisible(false);
|
||||
mDFTChart.setAlternativeRowFillVisible(false);
|
||||
mDFTChart.getStylesheets().add( css );
|
||||
|
||||
hbox.getChildren().add( mDFTChart );
|
||||
HBox.setHgrow( mDFTChart, Priority.ALWAYS );
|
||||
|
||||
setCenter( hbox );
|
||||
}
|
||||
|
||||
public void setImpulseTaps( double[] taps )
|
||||
{
|
||||
float[] convertedTaps = new float[ taps.length ];
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
{
|
||||
convertedTaps[ x ] = (float)taps[ x ];
|
||||
}
|
||||
|
||||
setImpulseTaps( convertedTaps );
|
||||
}
|
||||
|
||||
public void setImpulseTaps( float[] taps )
|
||||
{
|
||||
double smallest = 0.0;
|
||||
double largest = 0.0;
|
||||
|
||||
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
|
||||
URL url = getClass().getResource("/../resources/FilterView.css");
|
||||
|
||||
if(url != null)
|
||||
{
|
||||
if( taps[ x ] < smallest )
|
||||
{
|
||||
smallest = taps[ x ];
|
||||
}
|
||||
|
||||
if( taps[ x ] > largest )
|
||||
{
|
||||
largest = taps[ x ];
|
||||
}
|
||||
|
||||
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
|
||||
String css = url.toExternalForm();
|
||||
mImpulseChart.getStylesheets().add(css);
|
||||
mDFTChart.getStylesheets().add(css);
|
||||
}
|
||||
|
||||
mImpulseXAxis.setLowerBound( 0.0 );
|
||||
mImpulseXAxis.setUpperBound( taps.length - 1 );
|
||||
mImpulseXAxis.setTickUnit( 1.0 );
|
||||
|
||||
double padding = Math.abs( largest - smallest ) * .1;
|
||||
|
||||
mImpulseYAxis.setLowerBound( smallest - padding );
|
||||
mImpulseYAxis.setUpperBound( largest + padding );
|
||||
mImpulseYAxis.setTickUnit( padding );
|
||||
|
||||
hbox.getChildren().add(mDFTChart);
|
||||
HBox.setHgrow(mDFTChart, Priority.ALWAYS);
|
||||
|
||||
setCenter(hbox);
|
||||
}
|
||||
|
||||
public void setImpulseTaps(double[] taps)
|
||||
{
|
||||
float[] convertedTaps = new float[taps.length];
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
convertedTaps[x] = (float)taps[x];
|
||||
}
|
||||
|
||||
setImpulseTaps(convertedTaps);
|
||||
}
|
||||
|
||||
public void setImpulseTaps(float[] taps)
|
||||
{
|
||||
double smallest = 0.0;
|
||||
double largest = 0.0;
|
||||
|
||||
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
if(taps[x] < smallest)
|
||||
{
|
||||
smallest = taps[x];
|
||||
}
|
||||
|
||||
if(taps[x] > largest)
|
||||
{
|
||||
largest = taps[x];
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
|
||||
}
|
||||
|
||||
mImpulseXAxis.setLowerBound(0.0);
|
||||
mImpulseXAxis.setUpperBound(taps.length - 1);
|
||||
mImpulseXAxis.setTickUnit(1.0);
|
||||
|
||||
double padding = Math.abs(largest - smallest) * .1;
|
||||
|
||||
mImpulseYAxis.setLowerBound(smallest - padding);
|
||||
mImpulseYAxis.setUpperBound(largest + padding);
|
||||
mImpulseYAxis.setTickUnit(padding);
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mImpulseChart.getData();
|
||||
list.add( series );
|
||||
}
|
||||
|
||||
public void setDFTTaps( float[] taps )
|
||||
{
|
||||
double smallest = 0.0;
|
||||
|
||||
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
|
||||
|
||||
for( int x = 0; x < taps.length; x++ )
|
||||
{
|
||||
if( taps[ x ] < smallest )
|
||||
{
|
||||
smallest = taps[ x ];
|
||||
}
|
||||
|
||||
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
|
||||
}
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mDFTChart.getData();
|
||||
list.add( series );
|
||||
}
|
||||
|
||||
public float[] convertDFTToDecibel( float[] dft, int tapLength )
|
||||
list.add(series);
|
||||
}
|
||||
|
||||
public void setDFTTaps(float[] taps)
|
||||
{
|
||||
float[] decibels = new float[ dft.length / 2 ];
|
||||
double smallest = 0.0;
|
||||
|
||||
int index = 0;
|
||||
|
||||
for( int x = 0; x < decibels.length; x ++ )
|
||||
{
|
||||
index = x * 2;
|
||||
|
||||
decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) +
|
||||
( dft[ index + 1 ] * dft[ index + 1 ] ) ) );
|
||||
}
|
||||
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
|
||||
|
||||
return decibels;
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
if(taps[x] < smallest)
|
||||
{
|
||||
smallest = taps[x];
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
|
||||
}
|
||||
|
||||
ObservableList<Series<Number,Number>> list = mDFTChart.getData();
|
||||
list.add(series);
|
||||
}
|
||||
|
||||
public float[] convertDFTToDecibel(float[] dft, int tapLength)
|
||||
{
|
||||
float[] decibels = new float[dft.length / 2];
|
||||
|
||||
int index = 0;
|
||||
|
||||
for(int x = 0; x < decibels.length; x++)
|
||||
{
|
||||
index = x * 2;
|
||||
|
||||
decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) +
|
||||
(dft[index + 1] * dft[index + 1])));
|
||||
}
|
||||
|
||||
return decibels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package io.github.dsheirer.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
@@ -72,23 +73,29 @@ public class FilterViewer extends Application
|
||||
|
||||
float[] taps = null;
|
||||
|
||||
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
|
||||
// Window.WindowType.HAMMING, true);
|
||||
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
taps = designer.getImpulseResponse();
|
||||
}
|
||||
taps = FilterFactory.getSincM2Channelizer(12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
mLog.error("Error");
|
||||
}
|
||||
|
||||
// try
|
||||
// {
|
||||
// RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
//
|
||||
// if(designer.isValid())
|
||||
// {
|
||||
// taps = designer.getImpulseResponse();
|
||||
// }
|
||||
// }
|
||||
// catch(FilterDesignException fde)
|
||||
// {
|
||||
// mLog.error("Filter design error", fde);
|
||||
// }
|
||||
|
||||
return taps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.filter.fir.complex;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
@@ -6,49 +21,105 @@ import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class ComplexFIRFilter extends FIRFilter
|
||||
{
|
||||
private RealFIRFilter mIFilter;
|
||||
private RealFIRFilter mQFilter;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs. Wraps two real
|
||||
* FIR filters for processing each of the inphase and quadrature samples.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||
*/
|
||||
public ComplexFIRFilter( float[] coefficients, float gain )
|
||||
{
|
||||
mIFilter = new RealFIRFilter( coefficients, gain );
|
||||
mQFilter = new RealFIRFilter( coefficients, gain );
|
||||
}
|
||||
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mIFilter.getCoefficients();
|
||||
}
|
||||
|
||||
public float filterInphase( float sample )
|
||||
{
|
||||
return mIFilter.filter( sample );
|
||||
}
|
||||
|
||||
public float filterQuadrature( float sample )
|
||||
{
|
||||
return mQFilter.filter( sample );
|
||||
}
|
||||
|
||||
public Complex filter( Complex sample )
|
||||
{
|
||||
float i = filterInphase( sample.inphase() );
|
||||
float q = filterQuadrature( sample.quadrature() );
|
||||
|
||||
return new Complex( i, q );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
}
|
||||
private RealFIRFilter mIFilter;
|
||||
private RealFIRFilter mQFilter;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs. Wraps two real
|
||||
* FIR filters for processing each of the inphase and quadrature samples.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||
*/
|
||||
public ComplexFIRFilter(float[] coefficients, float gain)
|
||||
{
|
||||
mIFilter = new RealFIRFilter(coefficients, gain);
|
||||
mQFilter = new RealFIRFilter(coefficients, gain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coefficients used in each of the I and Q filters
|
||||
*/
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mIFilter.getCoefficients();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the inphase sample independent of the quadrature side of the filter
|
||||
* @param sample to filter
|
||||
* @return filtered inphase sample
|
||||
*/
|
||||
public float filterInphase(float sample)
|
||||
{
|
||||
return mIFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the quadrature sample independent of the inphase side of the filter
|
||||
* @param sample to filter
|
||||
* @return filtered quadrature sample
|
||||
*/
|
||||
public float filterQuadrature(float sample)
|
||||
{
|
||||
return mQFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the complex sample and provides a filtered output
|
||||
* @param sample to load
|
||||
* @return filtered complex sample
|
||||
*/
|
||||
public Complex filter(Complex sample)
|
||||
{
|
||||
return filter(sample.inphase(), sample.quadrature());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the complex sample and provides a filtered output
|
||||
* @param i inphase sample to load
|
||||
* @param q quadrature sample to load
|
||||
* @return filtered complex sample
|
||||
*/
|
||||
public Complex filter(float i, float q)
|
||||
{
|
||||
float filteredI = filterInphase(i);
|
||||
float filteredQ = filterQuadrature(q);
|
||||
|
||||
return new Complex(filteredI, filteredQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex sample array where the samples are loaded as i0,q0,i1,q1 ... iN-2,qN-1
|
||||
*
|
||||
* @param samples to filter.
|
||||
*/
|
||||
public float[] filter(float[] samples)
|
||||
{
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
samples[x] = filterInphase(samples[x]);
|
||||
samples[x + 1] = filterInphase(samples[x + 1]);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the complex sample into the filter but does not calculate a filtered output sample
|
||||
* @param i inphase sample
|
||||
* @param q quadrature sample
|
||||
*/
|
||||
public void load(float i, float q)
|
||||
{
|
||||
mIFilter.load(i);
|
||||
mQFilter.load(q);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.filter.fir.real;
|
||||
|
||||
@@ -21,85 +19,106 @@ import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
|
||||
public class RealFIRFilter extends FIRFilter
|
||||
{
|
||||
private float[] mBuffer;
|
||||
private float[] mBuffer;
|
||||
private int mBufferSize = 1;
|
||||
private int mBufferPointer = 0;
|
||||
private int[][] mIndexMap;
|
||||
|
||||
private float[] mCoefficients;
|
||||
private float mGain;
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class.
|
||||
*
|
||||
* @param coefficients - filter coefficients
|
||||
* @param gain value to apply to the filtered output - use 1.0f for no gain
|
||||
*/
|
||||
public RealFIRFilter( float[] coefficients, float gain )
|
||||
{
|
||||
mCoefficients = coefficients;
|
||||
mGain = gain;
|
||||
private int mBufferPointer = 0;
|
||||
private int[][] mIndexMap;
|
||||
|
||||
mBufferSize = mCoefficients.length;
|
||||
private float[] mCoefficients;
|
||||
private float mGain;
|
||||
|
||||
mBuffer = new float[ mBufferSize ];
|
||||
mBufferPointer = mBufferSize - 1;
|
||||
/**
|
||||
* Float sample FIR filter base class.
|
||||
*
|
||||
* @param coefficients - filter coefficients
|
||||
* @param gain value to apply to the filtered output - use 1.0f for no gain
|
||||
*/
|
||||
public RealFIRFilter(float[] coefficients, float gain)
|
||||
{
|
||||
mCoefficients = coefficients;
|
||||
mGain = gain;
|
||||
|
||||
generateIndexMap( mBufferSize );
|
||||
}
|
||||
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mCoefficients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mIndexMap = null;
|
||||
mBuffer = null;
|
||||
}
|
||||
mBufferSize = mCoefficients.length;
|
||||
|
||||
public float filter( float sample )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = sample;
|
||||
|
||||
float accumulator = 0.0f;
|
||||
mBuffer = new float[mBufferSize];
|
||||
mBufferPointer = mBufferSize - 1;
|
||||
|
||||
for( int x = 0; x < mBufferSize; x++ )
|
||||
{
|
||||
accumulator += mCoefficients[ x ] *
|
||||
mBuffer[ mIndexMap[ mBufferPointer][ x ] ];
|
||||
}
|
||||
generateIndexMap(mBufferSize);
|
||||
}
|
||||
|
||||
mBufferPointer--;
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mCoefficients;
|
||||
}
|
||||
|
||||
if( mBufferPointer < 0 )
|
||||
{
|
||||
mBufferPointer += mBufferSize;
|
||||
}
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mIndexMap = null;
|
||||
mBuffer = null;
|
||||
}
|
||||
|
||||
/* Apply gain and return the filtered value */
|
||||
return accumulator * mGain;
|
||||
}
|
||||
/**
|
||||
* Loads the sample into the circular buffer without calculating a filtered value.
|
||||
* @param sample to load
|
||||
*/
|
||||
public void load(float sample)
|
||||
{
|
||||
mBuffer[mBufferPointer] = sample;
|
||||
|
||||
/**
|
||||
* Generates a circular buffer index map to support lookup of the translated
|
||||
* index based on the current buffer pointer and the desired sample index.
|
||||
*/
|
||||
private void generateIndexMap( int size )
|
||||
{
|
||||
mIndexMap = new int[ size ][ size ];
|
||||
|
||||
for( int x = 0; x < size; x++ )
|
||||
{
|
||||
for( int y = 0; y < size; y++ )
|
||||
{
|
||||
int z = x + y;
|
||||
|
||||
mIndexMap[ x ][ y ] = z < size ? z : z - size;
|
||||
}
|
||||
}
|
||||
}
|
||||
mBufferPointer--;
|
||||
|
||||
if(mBufferPointer < 0)
|
||||
{
|
||||
mBufferPointer += mBufferSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the sample into the circular buffer and calculates a filtered sample output.
|
||||
* @param sample to load
|
||||
* @return filtered value
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
mBuffer[mBufferPointer] = sample;
|
||||
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for(int x = 0; x < mBufferSize; x++)
|
||||
{
|
||||
accumulator += mCoefficients[x] *
|
||||
mBuffer[mIndexMap[mBufferPointer][x]];
|
||||
}
|
||||
|
||||
mBufferPointer--;
|
||||
|
||||
if(mBufferPointer < 0)
|
||||
{
|
||||
mBufferPointer += mBufferSize;
|
||||
}
|
||||
|
||||
/* Apply gain and return the filtered value */
|
||||
return accumulator * mGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a circular buffer index map to support lookup of the translated
|
||||
* index based on the current buffer pointer and the desired sample index.
|
||||
*/
|
||||
private void generateIndexMap(int size)
|
||||
{
|
||||
mIndexMap = new int[size][size];
|
||||
|
||||
for(int x = 0; x < size; x++)
|
||||
{
|
||||
for(int y = 0; y < size; y++)
|
||||
{
|
||||
int z = x + y;
|
||||
|
||||
mIndexMap[x][y] = z < size ? z : z - size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
@@ -190,15 +191,9 @@ public class PolyphaseFIRDecimatingFilter_RB extends FIRFilter implements Listen
|
||||
}
|
||||
});
|
||||
|
||||
Oscillator oscillator = new Oscillator(3400, 48000);
|
||||
|
||||
float[] samples = new float[500];
|
||||
for(int x = 0; x < 500; x++)
|
||||
{
|
||||
samples[x] = oscillator.getFloat();
|
||||
oscillator.rotate();
|
||||
}
|
||||
IOscillator oscillator = new Oscillator(3400, 48000);
|
||||
|
||||
float[] samples = oscillator.generateReal(500);
|
||||
RealBuffer buffer = new RealBuffer(samples);
|
||||
filter.receive(buffer);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.github.dsheirer.dsp.gain;
|
||||
|
||||
import io.github.dsheirer.buffer.DoubleCircularBuffer;
|
||||
import io.github.dsheirer.buffer.FloatCircularBuffer;
|
||||
import io.github.dsheirer.buffer.RealCircularBuffer;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -131,8 +131,8 @@ public class AutomaticGainControl
|
||||
private double mAttackAverage = 0.0;
|
||||
private double mDecayAverage = 0.0;
|
||||
|
||||
private FloatCircularBuffer mDelayBuffer =
|
||||
new FloatCircularBuffer( (int)( SAMPLE_RATE * DELAY_TIME_CONSTANT ) );
|
||||
private RealCircularBuffer mDelayBuffer =
|
||||
new RealCircularBuffer( (int)( SAMPLE_RATE * DELAY_TIME_CONSTANT ) );
|
||||
private DoubleCircularBuffer mMagnitudeBuffer =
|
||||
new DoubleCircularBuffer( (int)( SAMPLE_RATE * WINDOW_TIME_CONSTANT ) );
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.github.dsheirer.dsp.gain;
|
||||
|
||||
import io.github.dsheirer.buffer.FloatCircularBuffer;
|
||||
import io.github.dsheirer.buffer.RealCircularBuffer;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
|
||||
private ComplexSampleListener mListener;
|
||||
|
||||
private FloatCircularBuffer mEnvelopeHistory;
|
||||
private RealCircularBuffer mEnvelopeHistory;
|
||||
|
||||
private float mMaxEnvelope = 0.0f;
|
||||
private float mGain = 1.0f;
|
||||
@@ -53,7 +53,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
*/
|
||||
public ComplexFeedForwardGainControl( int window )
|
||||
{
|
||||
mEnvelopeHistory = new FloatCircularBuffer( window );
|
||||
mEnvelopeHistory = new RealCircularBuffer( window );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public abstract class AbstractOscillator implements IOscillator
|
||||
{
|
||||
private boolean mEnabled;
|
||||
private double mFrequency;
|
||||
private double mSampleRate;
|
||||
|
||||
public AbstractOscillator(double frequency, double sampleRate)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
mSampleRate = sampleRate;
|
||||
mEnabled = frequency != 0;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency of the tone being generated by this oscillator
|
||||
*/
|
||||
public double getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the frequency of this oscillator
|
||||
*/
|
||||
public void setFrequency(double frequency)
|
||||
{
|
||||
mEnabled = frequency != 0;
|
||||
mFrequency = frequency;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample rate of this oscillator
|
||||
*/
|
||||
public double getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the sample rate of this oscillator
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this oscillator has a non-zero frequency setting. When used as a frequency translating
|
||||
* mixer, setting a frequency of zero indicates that no frequency correction is needed, so this flag can
|
||||
* indicate when to apply the oscillator to frequency correct samples.
|
||||
*
|
||||
* @return true if the current frequency value is non-zero.
|
||||
*/
|
||||
@Override
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
protected abstract void update();
|
||||
|
||||
/**
|
||||
* Performs complex heterodyne against the samples using this oscillator
|
||||
* @param samples to mix with this oscillator
|
||||
*/
|
||||
@Override
|
||||
public float[] mixComplex(float[] samples)
|
||||
{
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
float i = Complex.multiplyInphase(samples[x], samples[x + 1], inphase(), quadrature());
|
||||
float q = Complex.multiplyQuadrature(samples[x], samples[x + 1], inphase(), quadrature());
|
||||
|
||||
samples[x] = i;
|
||||
samples[x + 1] = q;
|
||||
|
||||
rotate();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of real samples from this oscillator.
|
||||
* @param sampleCount number of samples to generate and length of the resulting float array.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public float[] generateReal(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount];
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
samples[x] = inphase();
|
||||
rotate();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of complex samples from this oscillator.
|
||||
* @param sampleCount number of samples to generate and length of the resulting float array.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public float[] generateComplex(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount * 2];
|
||||
|
||||
for(int x = 0; x < sampleCount * 2; x += 2)
|
||||
{
|
||||
samples[x] = inphase();
|
||||
samples[x + 1] = quadrature();
|
||||
rotate();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
95
src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java
Normal file
95
src/main/java/io/github/dsheirer/dsp/mixer/IOscillator.java
Normal file
@@ -0,0 +1,95 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.dsp.mixer;
|
||||
|
||||
/**
|
||||
* Oscillator interface.
|
||||
*/
|
||||
public interface IOscillator
|
||||
{
|
||||
/**
|
||||
* Frequency of the tone being generated by the oscillator
|
||||
* @return frequency in hertz
|
||||
*/
|
||||
double getFrequency();
|
||||
|
||||
/**
|
||||
* Sets the frequency of the tone being generated by the oscillator.
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
void setFrequency(double frequency);
|
||||
|
||||
/**
|
||||
* Sample rate of the oscillator
|
||||
* @return sample rate in hertz
|
||||
*/
|
||||
double getSampleRate();
|
||||
|
||||
/**
|
||||
* Sets the sample rate of the oscillator
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
void setSampleRate(double sampleRate);
|
||||
|
||||
|
||||
/**
|
||||
* Indicates if this oscillator is enabled, meaning that it has a non-zero frequency value.
|
||||
* @return true if the frequency value is non-zero
|
||||
*/
|
||||
boolean isEnabled();
|
||||
|
||||
/**
|
||||
* Current quadrature/imaginary value of the oscillator. Note: if you are using this value iteratively, make sure
|
||||
* you invoke rotate() after each time you invoke this method.
|
||||
* @return current quadrature value of the oscillator.
|
||||
*/
|
||||
float quadrature();
|
||||
|
||||
/**
|
||||
* Current inphase/real value of the oscillator. Note: if you are using this value iteratively, make sure you
|
||||
* invoke rotate() after each time you invoke this method.
|
||||
* @return current inphase value of the oscillator.
|
||||
*/
|
||||
float inphase();
|
||||
|
||||
|
||||
/**
|
||||
* Rotates the oscillator by one sample period to generate new inphase() and quadrature() values.
|
||||
*/
|
||||
void rotate();
|
||||
|
||||
/**
|
||||
* Generates the specified number of real samples into a sample array.
|
||||
* @param sampleCount number of samples to generate and length of the resulting float array.
|
||||
* @return generated samples
|
||||
*/
|
||||
float[] generateReal(int sampleCount);
|
||||
|
||||
|
||||
/**
|
||||
* Generates the specified number of complex samples where the samples are arranged as i0,q0,i1,q1 ... iN-1,qN-1
|
||||
* @param sampleCount number of samples to generate.
|
||||
* @return generated complex samples with an array length of 2 * N
|
||||
*/
|
||||
float[] generateComplex(int sampleCount);
|
||||
|
||||
/**
|
||||
* Mixes (heterodynes) the complex sample array using the current settings of this oscillator.
|
||||
* @param complexSamples to mix to a new frequency
|
||||
* @return mixed/heterdyned samples
|
||||
*/
|
||||
float[] mixComplex(float[] complexSamples);
|
||||
}
|
||||
@@ -1,42 +1,36 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class LowPhaseNoiseOscillator
|
||||
public class LowPhaseNoiseOscillator extends AbstractOscillator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LowPhaseNoiseOscillator.class);
|
||||
|
||||
private static final double TWO_PI = 2.0 * Math.PI;
|
||||
private static final float THREE_HALVES = 3.0f / 2.0f;
|
||||
private int mSampleRate;
|
||||
private long mFrequency;
|
||||
private float mInphase = 1.0f;
|
||||
private float mQuadrature = 0.0f;
|
||||
private float mPreviousInphase = 1.0f;
|
||||
private float mPreviousQuadrature = 0.0f;
|
||||
private float mCosineAngle;
|
||||
private float mSineAngle;
|
||||
private float mGain = 1.0f;
|
||||
private static final double THREE_HALVES = 3.0 / 2.0;
|
||||
private double mInphase = 1.0;
|
||||
private double mQuadrature = 0.0;
|
||||
private double mPreviousInphase = 1.0;
|
||||
private double mPreviousQuadrature = 0.0;
|
||||
private double mCosineAngle;
|
||||
private double mSineAngle;
|
||||
private double mGain = 1.0;
|
||||
|
||||
/**
|
||||
* Ultra-low phase noise complex oscillator as described in Digital Signal Processing 3e, Lyons, p.786
|
||||
@@ -44,60 +38,40 @@ public class LowPhaseNoiseOscillator
|
||||
* @param sampleRate for the oscillator
|
||||
* @param frequency to generate
|
||||
*/
|
||||
public LowPhaseNoiseOscillator(int sampleRate, long frequency)
|
||||
public LowPhaseNoiseOscillator(double frequency, double sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mFrequency = frequency;
|
||||
super(frequency, sampleRate);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate for this oscillator
|
||||
*/
|
||||
public void setSampleRate(int sampleRate)
|
||||
@Override
|
||||
public float quadrature()
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
update();
|
||||
return (float)mQuadrature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample rate for this oscillator
|
||||
*/
|
||||
public int getSampleRate()
|
||||
@Override
|
||||
public float inphase()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tone frequency generated by this oscillator
|
||||
*/
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tone frequency for this oscillator
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
return (float)mInphase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
private void update()
|
||||
protected void update()
|
||||
{
|
||||
double anglePerSample = TWO_PI * mFrequency / mSampleRate;
|
||||
double anglePerSample = TWO_PI * getFrequency() / getSampleRate();
|
||||
|
||||
mCosineAngle = (float)Math.cos(anglePerSample);
|
||||
mSineAngle = (float)Math.sin(anglePerSample);
|
||||
}
|
||||
|
||||
private void rotate()
|
||||
/**
|
||||
* Rotates the oscillator to generate the next complex sample.
|
||||
*/
|
||||
public void rotate()
|
||||
{
|
||||
mInphase = ((mPreviousInphase * mCosineAngle) - (mPreviousQuadrature * mSineAngle)) * mGain;
|
||||
mQuadrature = ((mPreviousInphase * mSineAngle) + (mPreviousQuadrature * mCosineAngle)) * mGain;
|
||||
@@ -105,84 +79,7 @@ public class LowPhaseNoiseOscillator
|
||||
mPreviousInphase = mInphase;
|
||||
mPreviousQuadrature = mQuadrature;
|
||||
|
||||
//Update the gain value for the next rotation
|
||||
mGain = THREE_HALVES - ((mPreviousInphase * mPreviousInphase) + (mPreviousQuadrature * mPreviousQuadrature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of samples with length of 2 x sample count containing interleaved inphase and quadrature
|
||||
* samples.
|
||||
*
|
||||
* @param sampleCount number of samples to generate
|
||||
* @return array that is sampleCount * 2 in length
|
||||
*/
|
||||
public float[] generate(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount * 2];
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
int index = x * 2;
|
||||
samples[index] = mInphase;
|
||||
samples[index + 1] = mQuadrature;
|
||||
rotate();
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int sampleRate = 10000000;
|
||||
int frequency = 24000;
|
||||
int sampleSize = 65536;
|
||||
int warmUpIterations = 100000;
|
||||
int testIterations = 500000;
|
||||
|
||||
LowPhaseNoiseOscillator o = new LowPhaseNoiseOscillator(sampleRate, frequency);
|
||||
|
||||
mLog.debug("Warming up ...");
|
||||
|
||||
for(int x = 0; x < warmUpIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
mLog.debug("Testing ...");
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for(int x = 0; x < testIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
|
||||
double durationSeconds = (double)duration / 1000000000.0;
|
||||
mLog.debug("New Took:" + durationSeconds + " seconds to generate " + (testIterations * sampleSize) + " samples");
|
||||
|
||||
Oscillator o2 = new Oscillator(frequency, sampleRate);
|
||||
|
||||
mLog.debug("Warming up ...");
|
||||
|
||||
for(int x = 0; x < warmUpIterations; x++)
|
||||
{
|
||||
ComplexBuffer samples = o2.generateComplexBuffer(sampleSize);
|
||||
}
|
||||
|
||||
mLog.debug("Testing ...");
|
||||
|
||||
long start2 = System.nanoTime();
|
||||
|
||||
for(int x = 0; x < testIterations; x++)
|
||||
{
|
||||
float[] samples = o.generate(sampleSize);
|
||||
}
|
||||
|
||||
long duration2 = System.nanoTime() - start2;
|
||||
|
||||
double durationSeconds2 = (double)duration2 / 1000000000.0;
|
||||
mLog.debug("New Took:" + durationSeconds2 + " seconds to generate " + (testIterations * sampleSize) + " samples");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +1,68 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Oscillator
|
||||
public class Oscillator extends AbstractOscillator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(Oscillator.class);
|
||||
|
||||
private boolean mEnabled = true;
|
||||
private double mFrequency;
|
||||
private double mSampleRate;
|
||||
|
||||
private Complex mAnglePerSample;
|
||||
private Complex mCurrentAngle = new Complex(0.0f, -1.0f);
|
||||
|
||||
/**
|
||||
* Oscillator produces complex or float samples corresponding to a sine wave
|
||||
* oscillating at the specified frequency and sample rate
|
||||
* Oscillator that produces complex or float samples corresponding to a sine wave oscillating at the specified
|
||||
* frequency and sample rate
|
||||
*
|
||||
* @param frequency - positive or negative frequency in hertz
|
||||
* @param sampleRate - in hertz
|
||||
*/
|
||||
public Oscillator(long frequency, double sampleRate)
|
||||
public Oscillator(double frequency, double sampleRate)
|
||||
{
|
||||
mEnabled = frequency != 0;
|
||||
|
||||
mSampleRate = sampleRate;
|
||||
mFrequency = (double)frequency;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this oscillator has a non-zero frequency setting. When used as a frequency translating
|
||||
* mixer, setting a frequency of zero indicates that no frequency correction is needed, so this flag can
|
||||
* indicate when to apply the oscillator to frequency correct samples.
|
||||
*
|
||||
* @return true if the current frequency value is non-zero.
|
||||
*/
|
||||
public boolean enabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
float anglePerSample =
|
||||
(float)(2.0d * Math.PI * mFrequency / mSampleRate);
|
||||
|
||||
mAnglePerSample = Complex.fromAngle(anglePerSample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the frequency of this oscillator
|
||||
*/
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mEnabled = frequency != 0;
|
||||
mFrequency = (double)frequency;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the sample rate of this oscillator
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
update();
|
||||
super(frequency, sampleRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps the current angle by the angle per sample amount
|
||||
*/
|
||||
@Override
|
||||
public void rotate()
|
||||
{
|
||||
mCurrentAngle.multiply(mAnglePerSample);
|
||||
mCurrentAngle.fastNormalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float inphase()
|
||||
{
|
||||
return mCurrentAngle.inphase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float quadrature()
|
||||
{
|
||||
return mCurrentAngle.quadrature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next complex sample
|
||||
* Updates the internal values after a frequency or sample rate change
|
||||
*/
|
||||
public Complex getComplex()
|
||||
@Override
|
||||
protected void update()
|
||||
{
|
||||
return mCurrentAngle.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next float sample
|
||||
*/
|
||||
public float getFloat()
|
||||
{
|
||||
return mCurrentAngle.real();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the requested number of complex samples and returns them in a complex buffer
|
||||
*
|
||||
* @param sampleCount to generate
|
||||
* @return complex buffer containing the requested complex samples
|
||||
*/
|
||||
public ComplexBuffer generateComplexBuffer(int sampleCount)
|
||||
{
|
||||
float[] buffer = new float[sampleCount * 2];
|
||||
|
||||
for(int x = 0; x < sampleCount; x++)
|
||||
{
|
||||
int index = 2 * x;
|
||||
buffer[index] = inphase();
|
||||
buffer[index + 1] = quadrature();
|
||||
|
||||
rotate();
|
||||
}
|
||||
|
||||
return new ComplexBuffer(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complex heterodyne against the samples using this oscillator
|
||||
* @param samples to mix with this oscillator
|
||||
*/
|
||||
public void mixComplex(float[] samples)
|
||||
{
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
float i = Complex.multiplyInphase(samples[x], samples[x + 1], inphase(), quadrature());
|
||||
float q = Complex.multiplyQuadrature(samples[x], samples[x + 1], inphase(), quadrature());
|
||||
|
||||
samples[x] = i;
|
||||
samples[x + 1] = q;
|
||||
|
||||
rotate();
|
||||
}
|
||||
float anglePerSample = (float)(2.0d * Math.PI * getFrequency() / getSampleRate());
|
||||
mAnglePerSample = Complex.fromAngle(anglePerSample);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* Copyright (C) 2014-2018 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
|
||||
@@ -18,6 +18,7 @@ package io.github.dsheirer.source.tuner.channel;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.Buffer;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -47,7 +48,7 @@ public class CICTunerChannelSource extends TunerChannelSource
|
||||
|
||||
private OverflowableTransferQueue<ComplexBuffer> mBuffer;
|
||||
|
||||
private Oscillator mFrequencyCorrectionMixer;
|
||||
private IOscillator mFrequencyCorrectionMixer;
|
||||
private ComplexPrimeCICDecimate mDecimationFilter;
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
private List<ComplexBuffer> mSampleBuffers = new ArrayList<ComplexBuffer>();
|
||||
|
||||
@@ -174,6 +174,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
|
||||
{
|
||||
//Notification events from the producer
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
mLog.debug("Processing Source Event - setting frequency to " + sourceEvent.getValue().longValue());
|
||||
setFrequency(sourceEvent.getValue().longValue());
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.source.tuner.test;
|
||||
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.LowPhaseNoiseOscillator;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -32,7 +33,7 @@ public class SampleGenerator
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SampleGenerator.class);
|
||||
|
||||
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
|
||||
private LowPhaseNoiseOscillator mOscillator;
|
||||
private IOscillator mOscillator;
|
||||
private int mSweepUpdateInterval;
|
||||
private long mInterval;
|
||||
private int mSamplesPerInterval;
|
||||
@@ -57,7 +58,7 @@ public class SampleGenerator
|
||||
throw new IllegalArgumentException("Sweep update rate cannot be greater than sample rate");
|
||||
}
|
||||
|
||||
mOscillator = new LowPhaseNoiseOscillator(sampleRate, frequency);
|
||||
mOscillator = new LowPhaseNoiseOscillator(frequency, sampleRate);
|
||||
mInterval = interval;
|
||||
mSweepUpdateInterval = sweepUpdateRate;
|
||||
|
||||
@@ -175,7 +176,7 @@ public class SampleGenerator
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mOscillator.getFrequency();
|
||||
return (long)mOscillator.getFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,15 +214,15 @@ public class SampleGenerator
|
||||
mReusableBuffer.getSamples().length != mSamplesPerInterval ||
|
||||
mOscillatorFrequency != mOscillator.getFrequency())
|
||||
{
|
||||
mOscillatorFrequency = mOscillator.getFrequency();
|
||||
mReusableBuffer = new TimestampedComplexBuffer(mOscillator.generate(mSamplesPerInterval));
|
||||
mOscillatorFrequency = (long)mOscillator.getFrequency();
|
||||
mReusableBuffer = new TimestampedComplexBuffer(mOscillator.generateComplex(mSamplesPerInterval));
|
||||
}
|
||||
|
||||
mComplexBufferBroadcaster.receive(mReusableBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
float[] samples = mOscillator.generate(mSamplesPerInterval);
|
||||
float[] samples = mOscillator.generateComplex(mSamplesPerInterval);
|
||||
mComplexBufferBroadcaster.receive(new TimestampedComplexBuffer(samples));
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ public class SampleGenerator
|
||||
{
|
||||
mTriggerInterval = 0;
|
||||
|
||||
long updatedFrequency = mOscillator.getFrequency() + mSweepUpdateInterval;
|
||||
long updatedFrequency = (long)mOscillator.getFrequency() + mSweepUpdateInterval;
|
||||
|
||||
if(updatedFrequency > mOscillator.getSampleRate() / 2)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ public class TestTunerController extends TunerController
|
||||
|
||||
public static final long MINIMUM_FREQUENCY = 1l;
|
||||
public static final long MAXIMUM_FREQUENCY = 1000000000l;
|
||||
public static final int SAMPLE_RATE = 225000;
|
||||
public static final int SAMPLE_RATE = 50000;
|
||||
public static final int DC_NOISE_BANDWIDTH = 0;
|
||||
public static final double USABLE_BANDWIDTH = 1.00;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* ....Show License.... */
|
||||
.chart {
|
||||
-fx-background-image: url("../../../images/Curve-fitted-background.png");
|
||||
-fx-background-image: url("/../resources/images/Curve-fitted-background.png");
|
||||
-fx-padding: 15 25 15 15;
|
||||
}
|
||||
.chart-plot-background {
|
||||
-fx-background-color: red;
|
||||
-fx-background-image: url("../../../images/Curve-fitted-chart-background.png"),
|
||||
url("../../../images/Curve-fitted-graph-gridlines.png");
|
||||
-fx-background-image: url("/../resources/images/Curve-fitted-chart-background.png"),
|
||||
url("/../resources/images/Curve-fitted-graph-gridlines.png");
|
||||
-fx-background-size: cover, auto;
|
||||
-fx-background-repeat: no-repeat, repeat;
|
||||
-fx-background-position: 0% 0%, 0% 100%;
|
||||
|
||||
Reference in New Issue
Block a user