diff --git a/src/dsp/filter/channelizer/ChannelDistributor.java b/src/dsp/filter/channelizer/ChannelDistributor.java new file mode 100644 index 00000000..6bc5a84c --- /dev/null +++ b/src/dsp/filter/channelizer/ChannelDistributor.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package dsp.filter.channelizer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexBuffer; + +import java.util.HashMap; +import java.util.Map; + +public class ChannelDistributor +{ + private final static Logger mLog = LoggerFactory.getLogger(ChannelDistributor.class); + private Map mListenerMap = new HashMap<>(); + private int mBufferSize; + private int mChannelCount; + + public ChannelDistributor(int bufferSize, int channelCount) + { + mBufferSize = bufferSize; + mChannelCount = channelCount; + + } + + public void receive(float[] channels) + { + for(int channel = 0; channel < channels.length / 2; channel++) + { + if(mListenerMap.containsKey(channel)) + { + ComplexSampleAssembler channelAssembler = mListenerMap.get(channel); + + int index = channel * 2; + channelAssembler.receive(channels[index], channels[index + 1]); + } + } + } + + public void addListener(int channel, Listener listener) + { + ComplexSampleAssembler channelAssembler = mListenerMap.get(channel); + + if(channelAssembler == null) + { + channelAssembler = new ComplexSampleAssembler(mBufferSize); + mListenerMap.put(channel, channelAssembler); + } + + channelAssembler.addListener(listener); + } + + public class ComplexSampleAssembler + { + private Broadcaster mBroadcaster = new Broadcaster<>(); + private float[] mBuffer; + private int mBufferPointer; + private int mBufferSize; + private boolean mEnableLogging; + + public ComplexSampleAssembler(int bufferSize) + { + mBufferSize = bufferSize; + mBuffer = new float[mBufferSize]; + } + + public void receive(float inphase, float quadrature) + { + mBuffer[mBufferPointer++] = inphase; + mBuffer[mBufferPointer++] = inphase; + + if(mBufferPointer >= mBufferSize) + { + float[] bufferCopy = new float[mBuffer.length]; + System.arraycopy(mBuffer,0, bufferCopy, 0, mBuffer.length); + + mBroadcaster.receive(new ComplexBuffer(bufferCopy)); + + mBufferPointer = 0; + } + } + + public void addListener(Listener listener) + { + mBroadcaster.addListener(listener); + } + } +} diff --git a/src/dsp/filter/channelizer/ChannelizerViewer.java b/src/dsp/filter/channelizer/ChannelizerViewer.java index 94b8fb8c..17395e3e 100644 --- a/src/dsp/filter/channelizer/ChannelizerViewer.java +++ b/src/dsp/filter/channelizer/ChannelizerViewer.java @@ -19,6 +19,7 @@ package dsp.filter.channelizer; import dsp.filter.FilterFactory; +import dsp.mixer.LowPhaseNoiseOscillator; import dsp.mixer.Oscillator; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -35,6 +36,8 @@ import spectrum.SpectrumPanel; import spectrum.converter.ComplexDecibelConverter; import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import java.awt.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -45,14 +48,14 @@ public class ChannelizerViewer extends JFrame private final static Logger mLog = LoggerFactory.getLogger(ChannelizerViewer.class); private static final int CHANNEL_BANDWIDTH = 12500; + private static final int CHANNEL_FFT_FRAME_RATE = 20; private SettingsManager mSettingsManager = new SettingsManager(new TunerConfigurationModel()); private JPanel mPrimaryPanel; - private SpectrumPanel mSpectrumPanel; - private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); - private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); - private ChannelPanel mChannelPanel; - private Oscillator mOscillator; + private JPanel mControlPanel; + private ChannelPanel mPrimarySpectrumPanel; + private ChannelArrayPanel mChannelPanel; + private LowPhaseNoiseOscillator mOscillator; private int mChannelCount; private int mSampleRate; private int mToneFrequency; @@ -62,7 +65,7 @@ public class ChannelizerViewer extends JFrame { mChannelCount = channelCount; mSampleRate = mChannelCount * CHANNEL_BANDWIDTH; - mToneFrequency = 37500; //Set to channel 1 as default + mToneFrequency = (int)(CHANNEL_BANDWIDTH * 3); //Set to channel 1 as default init(); } @@ -76,13 +79,13 @@ public class ChannelizerViewer extends JFrame setLocationRelativeTo(null); add(getPrimaryPanel()); - mOscillator = new Oscillator(mToneFrequency, mSampleRate); + mOscillator = new LowPhaseNoiseOscillator(mSampleRate, mToneFrequency); } public void start() { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); - scheduledExecutorService.scheduleAtFixedRate(new SampleGenerationTask(), 0, 100, TimeUnit.MILLISECONDS); + scheduledExecutorService.scheduleAtFixedRate(new SampleGenerationTask(), 0, 50, TimeUnit.MILLISECONDS); } public void setToneFrequency(int frequency) @@ -93,22 +96,14 @@ public class ChannelizerViewer extends JFrame private void generateSamples() { - int size = mDFTSize.getSize(); + int size = mChannelCount * CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE * 2; - float[] samples = new float[size * 2]; - - for(int x = 0; x < size; x += 2) - { - samples[x] = mOscillator.getComplex().inphase(); - samples[x + 1] = mOscillator.getComplex().quadrature(); - - mOscillator.rotate(); - } + float[] samples = mOscillator.generate(size); ComplexBuffer buffer = new ComplexBuffer(samples); - getChannelPanel().receive(buffer); - mDFTProcessor.receive(buffer); + getChannelArrayPanel().receive(buffer); + getSpectrumPanel().receive(buffer); } private JPanel getPrimaryPanel() @@ -116,34 +111,60 @@ public class ChannelizerViewer extends JFrame if(mPrimaryPanel == null) { mPrimaryPanel = new JPanel(); - mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[][grow,fill]")); + mPrimaryPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[][][grow,fill]")); mPrimaryPanel.add(getSpectrumPanel(), "wrap"); - mPrimaryPanel.add(getChannelPanel()); + mPrimaryPanel.add(getControlPanel(), "wrap"); + mPrimaryPanel.add(getChannelArrayPanel()); } return mPrimaryPanel; } - private SpectrumPanel getSpectrumPanel() + private ChannelPanel getSpectrumPanel() { - if(mSpectrumPanel == null) + if(mPrimarySpectrumPanel == null) { - mSpectrumPanel = new SpectrumPanel(mSettingsManager); - mSpectrumPanel.setPreferredSize(new Dimension(1000, 200)); - mComplexDecibelConverter.addListener(mSpectrumPanel); - mDFTProcessor.addConverter(mComplexDecibelConverter); - mDFTProcessor.frequencyChanged(new FrequencyChangeEvent( - FrequencyChangeEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mSampleRate)); + mPrimarySpectrumPanel = new ChannelPanel(mSettingsManager, mSampleRate); + mPrimarySpectrumPanel.setPreferredSize(new Dimension(1000, 200)); + mPrimarySpectrumPanel.setDFTSize(DFTSize.FFT32768); } - return mSpectrumPanel; + return mPrimarySpectrumPanel; } - private ChannelPanel getChannelPanel() + private JPanel getControlPanel() + { + if(mControlPanel == null) + { + mControlPanel = new JPanel(); + + int maximumFrequency = mSampleRate / 2; + + SpinnerNumberModel model = new SpinnerNumberModel(mToneFrequency, -maximumFrequency, maximumFrequency, CHANNEL_BANDWIDTH ); + + model.addChangeListener(new ChangeListener() + { + @Override + public void stateChanged(ChangeEvent e) + { + int frequency = model.getNumber().intValue(); + mOscillator.setFrequency(frequency); + } + }); + + JSpinner spinner = new JSpinner(model); + + mControlPanel.add(spinner); + } + + return mControlPanel; + } + + private ChannelArrayPanel getChannelArrayPanel() { if(mChannelPanel == null) { - mChannelPanel = new ChannelPanel(getFilter()); + mChannelPanel = new ChannelArrayPanel(getFilter()); } return mChannelPanel; @@ -170,13 +191,47 @@ public class ChannelizerViewer extends JFrame return taps; } - public class ChannelPanel extends JPanel implements Listener + public class ChannelArrayPanel extends JPanel implements Listener { private PolyphaseChannelizer mPolyphaseChannelizer; + private ChannelDistributor mChannelDistributor; - public ChannelPanel(float[] taps) + public ChannelArrayPanel(float[] taps) { + int bufferSize = CHANNEL_BANDWIDTH / CHANNEL_FFT_FRAME_RATE; + if(bufferSize % 2 == 1) + { + bufferSize++; + } + mPolyphaseChannelizer = new PolyphaseChannelizer(taps, mChannelCount); + mChannelDistributor = new ChannelDistributor(bufferSize, mChannelCount); + mPolyphaseChannelizer.setChannelDistributor(mChannelDistributor); + + init(); + } + + private void init() + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill][grow,fill]", "")); + + for(int x = 0; x < mChannelCount; x++) + { + ChannelPanel channelPanel = new ChannelPanel(mSettingsManager, CHANNEL_BANDWIDTH * 2); + channelPanel.setPreferredSize(new Dimension(250,100)); + channelPanel.setDFTSize(DFTSize.FFT00512); + + if(x % 4 == 3) + { + add(channelPanel, "wrap"); + } + else + { + add(channelPanel); + } + + mChannelDistributor.addListener(x, channelPanel); + } } @Override @@ -186,6 +241,36 @@ public class ChannelizerViewer extends JFrame } } + public class ChannelPanel extends JPanel implements Listener + { + private DFTProcessor mDFTProcessor = new DFTProcessor(SampleType.COMPLEX); + private ComplexDecibelConverter mComplexDecibelConverter = new ComplexDecibelConverter(); + private SpectrumPanel mSpectrumPanel; + + public ChannelPanel(SettingsManager settingsManager, int sampleRate) + { + setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]")); + mSpectrumPanel = new SpectrumPanel(settingsManager); + add(mSpectrumPanel); + + mDFTProcessor.addConverter(mComplexDecibelConverter); + mDFTProcessor.frequencyChanged(new FrequencyChangeEvent( + FrequencyChangeEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate)); + mComplexDecibelConverter.addListener(mSpectrumPanel); + } + + public void setDFTSize(DFTSize dftSize) + { + mDFTProcessor.setDFTSize(dftSize); + } + + @Override + public void receive(ComplexBuffer complexBuffer) + { + mDFTProcessor.receive(complexBuffer); + } + } + public class SampleGenerator { public SampleGenerator() @@ -218,7 +303,9 @@ public class ChannelizerViewer extends JFrame public static void main(String[] args) { - final ChannelizerViewer frame = new ChannelizerViewer(16); + int channelCount = 24; + + final ChannelizerViewer frame = new ChannelizerViewer(channelCount); EventQueue.invokeLater(new Runnable() { diff --git a/src/dsp/filter/channelizer/PolyphaseChannelizer.java b/src/dsp/filter/channelizer/PolyphaseChannelizer.java index a41e07fb..cd6eeda8 100644 --- a/src/dsp/filter/channelizer/PolyphaseChannelizer.java +++ b/src/dsp/filter/channelizer/PolyphaseChannelizer.java @@ -43,6 +43,7 @@ public class PolyphaseChannelizer implements Listener private float[] mFilteredSamples; private boolean mToggle = false; private FloatFFT_1D mFFT; + private ChannelDistributor mChannelDistributor; private DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0000"); @@ -125,15 +126,15 @@ public class PolyphaseChannelizer implements Listener private void dispatch(float[] channels) { -// StringBuilder sb = new StringBuilder(); -// -// for(int x = 0; x < channels.length; x += 2) -// { -// sb.append((x/2)).append("[") -// .append(DECIMAL_FORMAT.format(magnitude(channels[x], channels[x + 1], mChannelCount))).append("] "); -// } -// -// mLog.debug("Out: " + sb.toString()); + if(mChannelDistributor != null) + { + mChannelDistributor.receive(channels); + } + } + + public void setChannelDistributor(ChannelDistributor channelDistributor) + { + mChannelDistributor = channelDistributor; } private double magnitude(float real, float imaginary, int fftSize) diff --git a/src/dsp/mixer/LowPhaseNoiseOscillator.java b/src/dsp/mixer/LowPhaseNoiseOscillator.java new file mode 100644 index 00000000..fcbbf0c5 --- /dev/null +++ b/src/dsp/mixer/LowPhaseNoiseOscillator.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package dsp.mixer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +public class LowPhaseNoiseOscillator +{ + 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 int 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; + + /** + * Ultra-low phase noise complex oscillator as described in Digital Signal Processing 3e, Lyons, p.786 + * + * @param sampleRate for the oscillator + * @param frequency to generate + */ + public LowPhaseNoiseOscillator(int sampleRate, int frequency) + { + mSampleRate = sampleRate; + mFrequency = frequency; + + update(); + } + + public void setSampleRate(int sampleRate) + { + mSampleRate = sampleRate; + update(); + } + + public void setFrequency(int frequency) + { + mFrequency = frequency; + update(); + } + + /** + * Updates the internal values after a frequency or sample rate change + */ + private void update() + { + double anglePerSample = TWO_PI * mFrequency / mSampleRate; + + mCosineAngle = (float)Math.cos(anglePerSample); + mSineAngle = (float)Math.sin(anglePerSample); + } + + private void rotate() + { + mInphase = ((mPreviousInphase * mCosineAngle) - (mPreviousQuadrature * mSineAngle)) * mGain; + mQuadrature = ((mPreviousInphase * mSineAngle) + (mPreviousQuadrature * mCosineAngle)) * mGain; + + mPreviousInphase = mInphase; + mPreviousQuadrature = mQuadrature; + + 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) + { + LowPhaseNoiseOscillator o = new LowPhaseNoiseOscillator(10, 1); + + float[] samples = o.generate(20); + + StringBuilder sb = new StringBuilder(); + for(int x = 0; x < samples.length; x += 2) + { + sb.append(samples[x]).append(" ").append(samples[x + 1]).append("\n"); + } + + mLog.debug("Samples:\n" + sb.toString()); + } +} diff --git a/src/dsp/mixer/Oscillator.java b/src/dsp/mixer/Oscillator.java index 19736800..26c6ac6a 100644 --- a/src/dsp/mixer/Oscillator.java +++ b/src/dsp/mixer/Oscillator.java @@ -21,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.complex.Complex; +import sample.complex.ComplexBuffer; public class Oscillator { @@ -110,4 +111,25 @@ public class Oscillator { 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); + } }