Updates the Oscillator and LowPhaseNoiseOscillator classes to share a

common interface and abstract class.
This commit is contained in:
Denny
2018-01-27 04:41:01 -05:00
parent 0cf964fa98
commit e7f7d27a1f
33 changed files with 1471 additions and 680 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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];
}

View File

@@ -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;
}

View File

@@ -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);
}
}
/**

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());

View File

@@ -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

View File

@@ -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();
});
}
}

View File

@@ -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!!");
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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 ) );

View File

@@ -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()

View File

@@ -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;
}
}

View 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);
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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>();

View File

@@ -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:

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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%;